diff --git a/fdGamer/.gitignore b/fdGamer/.gitignore new file mode 100644 index 00000000..3820a95c --- /dev/null +++ b/fdGamer/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/fdGamer/.metadata b/fdGamer/.metadata new file mode 100644 index 00000000..3d2d7d12 --- /dev/null +++ b/fdGamer/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "f6ff1529fd6d8af5f706051d9251ac9231c83407" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + - platform: windows + create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/fdGamer/README.md b/fdGamer/README.md new file mode 100644 index 00000000..003a6b71 --- /dev/null +++ b/fdGamer/README.md @@ -0,0 +1,16 @@ +# fd_gamer + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/fdGamer/analysis_options.yaml b/fdGamer/analysis_options.yaml new file mode 100644 index 00000000..e3476bd8 --- /dev/null +++ b/fdGamer/analysis_options.yaml @@ -0,0 +1,32 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +analyzer: + exclude: + - third_party/** + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/fdGamer/assets/bansonic icon.png b/fdGamer/assets/bansonic icon.png new file mode 100644 index 00000000..55a53e79 Binary files /dev/null and b/fdGamer/assets/bansonic icon.png differ diff --git a/fdGamer/assets/font.otf b/fdGamer/assets/font.otf new file mode 100644 index 00000000..278ac7ce Binary files /dev/null and b/fdGamer/assets/font.otf differ diff --git a/fdGamer/lib/main.dart b/fdGamer/lib/main.dart new file mode 100644 index 00000000..776b3b81 --- /dev/null +++ b/fdGamer/lib/main.dart @@ -0,0 +1,1124 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:webview_windows/webview_windows.dart'; +import 'package:window_manager/window_manager.dart'; + +const int kControlPort = 18961; +const Size kWindowSize = Size(1280, 820); +const String kWindowTitle = '浮动小游戏'; +const String kDefaultServerBase = 'http://127.0.0.1:8080'; +const String kAppFontFamily = 'Bansonic'; + +Future main(List args) async { + WidgetsFlutterBinding.ensureInitialized(); + await windowManager.ensureInitialized(); + + final config = _parseLaunchConfig(args); + const windowOptions = WindowOptions( + size: kWindowSize, + center: true, + backgroundColor: Colors.transparent, + skipTaskbar: false, + titleBarStyle: TitleBarStyle.normal, + title: kWindowTitle, + ); + + windowManager.waitUntilReadyToShow(windowOptions, () async { + await windowManager.setTitle(kWindowTitle); + await windowManager.show(); + await windowManager.focus(); + }); + + runApp(FloatingMiniGamesApp(config: config)); +} + +AppLaunchConfig _parseLaunchConfig(List args) { + String serverBase = kDefaultServerBase; + String? initialUrl; + + for (final rawArg in args) { + final arg = rawArg.trim(); + if (arg.isEmpty) { + continue; + } + if (arg.startsWith('--server-base=')) { + final value = arg.substring('--server-base='.length).trim(); + if (value.isNotEmpty) { + serverBase = _normalizeServerBase(value); + } + continue; + } + if (arg.startsWith('--open-url=')) { + final value = arg.substring('--open-url='.length).trim(); + if (value.isNotEmpty) { + initialUrl = _normalizeOpenTarget(value); + } + continue; + } + + initialUrl = _normalizeOpenTarget(arg); + } + + return AppLaunchConfig( + serverBase: _normalizeServerBase(serverBase), + initialUrl: initialUrl, + ); +} + +String _normalizeServerBase(String raw) { + final uri = _parseServerBaseUri(raw); + return '${uri.scheme}://${uri.authority}'; +} + +Uri _parseServerBaseUri(String raw) { + final trimmed = raw.trim().replaceAll('"', '').replaceAll("'", ''); + if (trimmed.isEmpty) { + return Uri.parse(kDefaultServerBase); + } + + Uri? parsed = Uri.tryParse(trimmed); + if (parsed != null && parsed.hasScheme && parsed.host.isNotEmpty) { + return parsed.replace(path: '', query: '', fragment: ''); + } + + parsed = Uri.tryParse('http://$trimmed'); + if (parsed != null && parsed.host.isNotEmpty) { + return parsed.replace(path: '', query: '', fragment: ''); + } + + return Uri.parse(kDefaultServerBase); +} + +Uri _buildServerUri(String serverBase, String path) { + final base = _parseServerBaseUri(serverBase); + final normalizedPath = path.startsWith('/') ? path : '/$path'; + return base.replace(path: normalizedPath, query: null, fragment: null); +} + +String _normalizeOpenTarget(String raw) { + final trimmed = raw.trim(); + if (trimmed.isEmpty) { + return trimmed; + } + + final uri = Uri.tryParse(trimmed); + if (uri != null && uri.hasScheme) { + return uri.toString(); + } + + final file = File(trimmed); + if (file.existsSync()) { + return file.absolute.uri.toString(); + } + + if (trimmed.startsWith(r'\\') || RegExp(r'^[A-Za-z]:[\\/]').hasMatch(trimmed)) { + return File(trimmed).absolute.uri.toString(); + } + + return trimmed; +} + +class AppLaunchConfig { + const AppLaunchConfig({required this.serverBase, this.initialUrl}); + + final String serverBase; + final String? initialUrl; +} + +class MiniGameRecord { + const MiniGameRecord({ + required this.id, + required this.name, + required this.gameType, + required this.publisher, + required this.publishDate, + required this.description, + required this.playUrl, + }); + + final int id; + final String name; + final String gameType; + final String publisher; + final String publishDate; + final String description; + final String playUrl; + + factory MiniGameRecord.fromJson(Map json, String serverBase) { + final rawPlayUrl = (json['play_url'] ?? '').toString().trim(); + return MiniGameRecord( + id: int.tryParse((json['id'] ?? '0').toString()) ?? 0, + name: (json['name'] ?? '').toString(), + gameType: ((json['game_type'] ?? 'official').toString().trim().toLowerCase() == 'community') ? 'community' : 'official', + publisher: (json['publisher'] ?? '').toString(), + publishDate: (json['publish_date'] ?? '').toString(), + description: (json['description'] ?? '').toString(), + playUrl: _absolutizePlayUrl(serverBase, rawPlayUrl), + ); + } +} + +String _absolutizePlayUrl(String serverBase, String playUrl) { + if (playUrl.isEmpty) { + return playUrl; + } + final uri = Uri.tryParse(playUrl); + if (uri != null && uri.hasScheme) { + return uri.toString(); + } + final slashNormalized = playUrl.startsWith('/') ? playUrl : '/$playUrl'; + return '${serverBase.replaceAll(RegExp(r'/+$'), '')}$slashNormalized'; +} + +class FloatingMiniGamesApp extends StatelessWidget { + const FloatingMiniGamesApp({super.key, required this.config}); + + final AppLaunchConfig config; + + @override + Widget build(BuildContext context) { + final baseTheme = ThemeData( + useMaterial3: true, + brightness: Brightness.dark, + fontFamily: kAppFontFamily, + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFF59C6FF), + brightness: Brightness.dark, + ), + scaffoldBackgroundColor: const Color(0xFF0B0D14), + snackBarTheme: const SnackBarThemeData( + behavior: SnackBarBehavior.floating, + ), + ); + + return MaterialApp( + title: kWindowTitle, + debugShowCheckedModeBanner: false, + theme: baseTheme, + home: MiniGamesShell(config: config), + ); + } +} + +class MiniGamesShell extends StatefulWidget { + const MiniGamesShell({super.key, required this.config}); + + final AppLaunchConfig config; + + @override + State createState() => _MiniGamesShellState(); +} + +class _MiniGamesShellState extends State { + final WebviewController _controller = WebviewController(); + final TextEditingController _searchController = TextEditingController(); + + HttpServer? _controlServer; + StreamSubscription? _titleSubscription; + StreamSubscription? _loadingSubscription; + StreamSubscription? _urlSubscription; + StreamSubscription? _historySubscription; + StreamSubscription? _loadErrorSubscription; + StreamSubscription? _webMessageSubscription; + Timer? _latencyTimer; + + bool _initialized = false; + bool _isLoading = false; + bool _showHome = true; + bool _gamesLoading = true; + bool _canGoBack = false; + bool _canGoForward = false; + bool _disposed = false; + + String _statusText = '正在初始化浏览器'; + String _currentLabel = '小游戏大厅'; + String _currentUrl = ''; + String _homeError = ''; + String _searchQuery = ''; + String _typeFilter = 'all'; + String? _queuedOpenUrl; + int? _latencyMs; + List _games = const []; + + @override + void initState() { + super.initState(); + _bootstrap(); + } + + Future _bootstrap() async { + try { + _searchController.addListener(() { + if (!mounted) { + return; + } + setState(() { + _searchQuery = _searchController.text.trim().toLowerCase(); + }); + }); + await _controller.initialize(); + await _controller.setBackgroundColor(const Color(0xFF0B0D14)); + await _controller.setPopupWindowPolicy(WebviewPopupWindowPolicy.deny); + await _installBrowserGuards(); + _bindControllerStreams(); + await _startControlServer(); + await _refreshLatency(); + _latencyTimer = Timer.periodic(const Duration(seconds: 15), (_) => _refreshLatency()); + await _loadMiniGames(); + if (widget.config.initialUrl != null && widget.config.initialUrl!.isNotEmpty) { + await _openUrl(widget.config.initialUrl!, label: '外部页面'); + } + if (!mounted) { + return; + } + setState(() { + _initialized = true; + _statusText = '网页已加载'; + }); + if (_queuedOpenUrl != null && _queuedOpenUrl!.isNotEmpty) { + final pending = _queuedOpenUrl!; + _queuedOpenUrl = null; + await _openUrl(pending, label: '外部页面'); + } + } catch (error) { + if (!mounted) { + return; + } + setState(() { + _homeError = '初始化失败:$error'; + _gamesLoading = false; + _statusText = '初始化失败'; + }); + } + } + + void _bindControllerStreams() { + _loadingSubscription = _controller.loadingState.listen((state) { + if (!mounted) { + return; + } + setState(() { + _isLoading = state != LoadingState.navigationCompleted; + if (!_showHome) { + _statusText = _isLoading ? '页面加载中' : '页面已就绪'; + } + }); + }); + + _urlSubscription = _controller.url.listen((url) { + if (!mounted) { + return; + } + setState(() { + _currentUrl = url; + }); + }); + + _titleSubscription = _controller.title.listen((title) { + if (!mounted || title.trim().isEmpty) { + return; + } + setState(() { + if (!_showHome) { + _currentLabel = title.trim(); + } + }); + }); + + _historySubscription = _controller.historyChanged.listen((history) { + if (!mounted) { + return; + } + setState(() { + _canGoBack = history.canGoBack; + _canGoForward = history.canGoForward; + }); + }); + + _loadErrorSubscription = _controller.onLoadError.listen((status) { + _showNotice('页面加载失败:${_describeWebError(status)}'); + }); + + _webMessageSubscription = _controller.webMessage.listen((dynamic message) { + if (message is Map) { + _handleBrowserMessage(message.map((key, value) => MapEntry(key.toString(), value))); + } + }); + } + + Future _installBrowserGuards() async { + await _controller.addScriptToExecuteOnDocumentCreated(''' + (() => { + const send = (payload) => { + try { + if (window.chrome && window.chrome.webview) { + window.chrome.webview.postMessage(JSON.stringify(payload)); + } + } catch (_) {} + }; + + const blockKeys = (event) => { + const key = (event.key || '').toLowerCase(); + const isF12 = key === 'f12'; + const isDevToolsCombo = event.ctrlKey && event.shiftKey && ['i', 'j', 'c'].includes(key); + const isSourceCombo = event.ctrlKey && key === 'u'; + if (isF12 || isDevToolsCombo || isSourceCombo) { + event.preventDefault(); + event.stopPropagation(); + send({ kind: 'blocked-shortcut', key: key }); + return false; + } + return true; + }; + + document.addEventListener('keydown', blockKeys, true); + document.addEventListener('contextmenu', (event) => event.preventDefault(), true); + document.addEventListener('wheel', (event) => { + if (event.ctrlKey) { + event.preventDefault(); + } + }, { passive: false, capture: true }); + + window.addEventListener('error', (event) => { + send({ + kind: 'js-error', + message: event.message || '脚本异常', + source: event.filename || '', + line: event.lineno || 0, + column: event.colno || 0 + }); + }, true); + + window.addEventListener('unhandledrejection', (event) => { + const reason = event && event.reason ? (event.reason.message || String(event.reason)) : 'Promise 未处理异常'; + send({ kind: 'promise-error', message: reason }); + }); + + const originalFetch = window.fetch ? window.fetch.bind(window) : null; + if (originalFetch) { + window.fetch = async (...args) => { + try { + const response = await originalFetch(...args); + if (!response.ok) { + send({ kind: 'http-error', url: response.url || '', status: response.status, statusText: response.statusText || '' }); + } + return response; + } catch (error) { + send({ kind: 'request-error', url: String(args[0] || ''), message: error && error.message ? error.message : String(error) }); + throw error; + } + }; + } + + const originalOpen = XMLHttpRequest.prototype.open; + XMLHttpRequest.prototype.open = function(method, url, ...rest) { + this.__fdRequestedUrl = url; + return originalOpen.call(this, method, url, ...rest); + }; + + const originalSend = XMLHttpRequest.prototype.send; + XMLHttpRequest.prototype.send = function(...args) { + this.addEventListener('error', () => { + send({ kind: 'request-error', url: String(this.__fdRequestedUrl || ''), message: 'XMLHttpRequest 失败' }); + }); + this.addEventListener('loadend', () => { + if (this.status >= 400) { + send({ kind: 'http-error', url: String(this.responseURL || this.__fdRequestedUrl || ''), status: this.status, statusText: this.statusText || '' }); + } + }); + return originalSend.apply(this, args); + }; + })(); + '''); + } + + Future _startControlServer() async { + _controlServer = await HttpServer.bind(InternetAddress.loopbackIPv4, kControlPort); + unawaited(_listenForControlRequests(_controlServer!)); + } + + Future _listenForControlRequests(HttpServer server) async { + await for (final request in server) { + try { + final path = request.uri.path; + if (request.method == 'GET' && path == '/health') { + await _writeJson(request.response, HttpStatus.ok, { + 'ok': true, + 'initialized': _initialized, + 'showHome': _showHome, + 'status': _statusText, + 'latencyMs': _latencyMs, + 'currentUrl': _currentUrl, + 'serverBase': widget.config.serverBase, + }); + continue; + } + + if (path == '/open' && (request.method == 'POST' || request.method == 'GET')) { + final targetUrl = await _extractUrl(request); + if (targetUrl == null || targetUrl.trim().isEmpty) { + await _writeJson(request.response, HttpStatus.badRequest, {'ok': false, 'message': 'missing url'}); + continue; + } + final normalized = _normalizeOpenTarget(targetUrl); + if (!_initialized) { + _queuedOpenUrl = normalized; + await _writeJson(request.response, HttpStatus.accepted, {'ok': true, 'queued': true, 'url': normalized}); + continue; + } + await _openUrl(normalized, label: '外部页面'); + await _writeJson(request.response, HttpStatus.ok, {'ok': true, 'queued': false, 'url': normalized}); + continue; + } + + if (path == '/home' && (request.method == 'POST' || request.method == 'GET')) { + await _goHome(); + await _writeJson(request.response, HttpStatus.ok, {'ok': true}); + continue; + } + + if (path == '/back' && (request.method == 'POST' || request.method == 'GET')) { + if (_canGoBack && !_showHome) { + await _controller.goBack(); + } + await _writeJson(request.response, HttpStatus.ok, {'ok': true, 'canGoBack': _canGoBack}); + continue; + } + + if (path == '/forward' && (request.method == 'POST' || request.method == 'GET')) { + if (_canGoForward && !_showHome) { + await _controller.goForward(); + } + await _writeJson(request.response, HttpStatus.ok, {'ok': true, 'canGoForward': _canGoForward}); + continue; + } + + if (path == '/refresh' && (request.method == 'POST' || request.method == 'GET')) { + await _refreshCurrent(); + await _writeJson(request.response, HttpStatus.ok, {'ok': true}); + continue; + } + + await _writeJson(request.response, HttpStatus.notFound, {'ok': false, 'message': 'not found'}); + } catch (error) { + await _writeJson(request.response, HttpStatus.internalServerError, {'ok': false, 'message': error.toString()}); + } + } + } + + Future _extractUrl(HttpRequest request) async { + if (request.method == 'GET') { + return request.uri.queryParameters['url']; + } + + final body = await utf8.decoder.bind(request).join(); + if (body.trim().isEmpty) { + return null; + } + + try { + final decoded = jsonDecode(body); + if (decoded is Map) { + return decoded['url']?.toString(); + } + if (decoded is Map) { + return decoded['url']?.toString(); + } + } catch (_) { + return body.trim(); + } + return null; + } + + Future _writeJson(HttpResponse response, int statusCode, Map payload) async { + response.statusCode = statusCode; + response.headers.contentType = ContentType.json; + response.write(jsonEncode(payload)); + await response.close(); + } + + Future _refreshLatency() async { + final stopwatch = Stopwatch()..start(); + try { + final uri = _buildServerUri(widget.config.serverBase, '/api/ping'); + final client = HttpClient()..connectionTimeout = const Duration(seconds: 5); + try { + final request = await client.getUrl(uri); + final response = await request.close().timeout(const Duration(seconds: 5)); + await response.drain(); + stopwatch.stop(); + if (mounted) { + setState(() { + _latencyMs = stopwatch.elapsedMilliseconds; + }); + } + } finally { + client.close(force: true); + } + } catch (_) { + if (mounted) { + setState(() { + _latencyMs = null; + }); + } + } + } + + Future _loadMiniGames() async { + if (mounted) { + setState(() { + _gamesLoading = true; + _homeError = ''; + }); + } + + try { + final client = HttpClient()..connectionTimeout = const Duration(seconds: 8); + try { + final candidates = [ + _buildServerUri(widget.config.serverBase, '/api/mini-games').toString(), + _buildServerUri(widget.config.serverBase, '/api/minigames').toString(), + _buildServerUri(widget.config.serverBase, '/api/easy-games').toString(), + _buildServerUri(widget.config.serverBase, '/mini-games/list').toString(), + _buildServerUri(widget.config.serverBase, '/minigames').toString(), + _buildServerUri(widget.config.serverBase, '/easy-games/list').toString(), + _buildServerUri(widget.config.serverBase, '/easygames').toString(), + _buildServerUri(widget.config.serverBase, '/admin/api/mini-games').toString(), + ]; + + HttpException? lastHttpError; + String body = ''; + int statusCode = 0; + Map? decoded; + for (final candidate in candidates) { + final request = await client.getUrl(Uri.parse(candidate)); + final response = await request.close().timeout(const Duration(seconds: 8)); + body = await utf8.decoder.bind(response).join(); + statusCode = response.statusCode; + if (response.statusCode == HttpStatus.ok) { + final rawDecoded = jsonDecode(body); + if (rawDecoded is Map) { + decoded = rawDecoded; + break; + } + if (rawDecoded is Map) { + decoded = rawDecoded.cast(); + break; + } + throw HttpException('HTTP 200: invalid payload'); + } + lastHttpError = HttpException('HTTP ${response.statusCode}: $body'); + if (response.statusCode != HttpStatus.notFound) { + break; + } + } + if (decoded == null) { + throw lastHttpError ?? HttpException('HTTP $statusCode: $body'); + } + final list = decoded['games']; + final items = []; + if (list is List) { + for (final entry in list) { + if (entry is Map) { + items.add(MiniGameRecord.fromJson(entry.cast(), widget.config.serverBase)); + } + } + } + if (!mounted) { + return; + } + setState(() { + _games = items; + _gamesLoading = false; + _statusText = '小游戏大厅已同步'; + }); + } finally { + client.close(force: true); + } + } catch (error) { + if (!mounted) { + return; + } + setState(() { + _gamesLoading = false; + _homeError = '无法拉取小游戏列表:$error'; + _statusText = '无法访问小游戏服务'; + }); + } + } + + Future _openGame(MiniGameRecord game) async { + await _openUrl(game.playUrl, label: game.name); + } + + Future _openUrl(String rawUrl, {String? label}) async { + final normalized = _normalizeOpenTarget(rawUrl); + if (!_initialized) { + _queuedOpenUrl = normalized; + return; + } + await _controller.loadUrl(normalized); + if (!mounted) { + return; + } + setState(() { + _showHome = false; + _isLoading = true; + _currentUrl = normalized; + _currentLabel = (label == null || label.trim().isEmpty) ? '网页内容' : label.trim(); + _statusText = '正在打开页面'; + }); + await windowManager.show(); + await windowManager.focus(); + } + + Future _goHome() async { + if (!_showHome) { + try { + await _controller.stop(); + } catch (_) {} + } + if (!mounted) { + return; + } + setState(() { + _showHome = true; + _isLoading = false; + _canGoBack = false; + _canGoForward = false; + _currentUrl = ''; + _currentLabel = '小游戏大厅'; + _statusText = '小游戏大厅已就绪'; + }); + await _loadMiniGames(); + } + + Future _refreshCurrent() async { + if (_showHome) { + await _loadMiniGames(); + return; + } + try { + await _controller.reload(); + } catch (_) {} + } + + String _describeWebError(WebErrorStatus status) { + switch (status) { + case WebErrorStatus.WebErrorStatusTimeout: + return '请求超时'; + case WebErrorStatus.WebErrorStatusConnectionAborted: + return '连接被中止'; + case WebErrorStatus.WebErrorStatusConnectionReset: + return '连接被重置'; + case WebErrorStatus.WebErrorStatusCannotConnect: + return '连接失败'; + case WebErrorStatus.WebErrorStatusDisconnected: + return '网络已断开'; + case WebErrorStatus.WebErrorStatusHostNameNotResolved: + return '无法解析主机名'; + case WebErrorStatus.WebErrorStatusServerUnreachable: + return '服务器不可达'; + case WebErrorStatus.WebErrorStatusErrorHTTPInvalidServerResponse: + return '服务器响应无效'; + default: + return status.name; + } + } + + void _handleBrowserMessage(Map message) { + final kind = (message['kind'] ?? '').toString(); + switch (kind) { + case 'blocked-shortcut': + _showNotice('已禁用开发者快捷键'); + break; + case 'js-error': + _showNotice('页面脚本异常:${message['message'] ?? '未知错误'}'); + break; + case 'promise-error': + _showNotice('页面 Promise 异常:${message['message'] ?? '未知错误'}'); + break; + case 'http-error': + final status = message['status']?.toString() ?? '--'; + final url = message['url']?.toString() ?? ''; + _showNotice('页面请求失败:HTTP $status${url.isEmpty ? '' : ' · $url'}'); + break; + case 'request-error': + final url = message['url']?.toString() ?? ''; + final detail = message['message']?.toString() ?? '网络请求异常'; + _showNotice('页面请求异常:$detail${url.isEmpty ? '' : ' · $url'}'); + break; + default: + break; + } + } + + void _showNotice(String message) { + if (!mounted || _disposed) { + return; + } + final messenger = ScaffoldMessenger.maybeOf(context); + messenger?.hideCurrentSnackBar(); + messenger?.showSnackBar( + SnackBar( + content: Text(message), + duration: const Duration(seconds: 3), + ), + ); + } + + @override + void dispose() { + _disposed = true; + _searchController.dispose(); + _latencyTimer?.cancel(); + _titleSubscription?.cancel(); + _loadingSubscription?.cancel(); + _urlSubscription?.cancel(); + _historySubscription?.cancel(); + _loadErrorSubscription?.cancel(); + _webMessageSubscription?.cancel(); + _controlServer?.close(force: true); + unawaited(_controller.dispose()); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final latencyText = _latencyMs == null ? '延迟 --' : '延迟 $_latencyMs ms'; + return Scaffold( + body: Column( + children: [ + Container( + height: 64, + decoration: const BoxDecoration( + color: Color(0xFF121826), + border: Border(bottom: BorderSide(color: Color(0x222D4B73))), + ), + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Image.asset( + 'assets/bansonic icon.png', + width: 36, + height: 36, + fit: BoxFit.cover, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + kWindowTitle, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700), + ), + Text( + _currentLabel, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 12, color: Color(0xFFAFC2DE)), + ), + ], + ), + ), + Text( + latencyText, + style: const TextStyle(fontSize: 12, color: Color(0xFF8FB3D9)), + ), + const SizedBox(width: 10), + IconButton( + tooltip: '刷新', + onPressed: _refreshCurrent, + icon: const Icon(Icons.refresh_rounded), + ), + IconButton( + tooltip: '后退', + onPressed: (!_showHome && _canGoBack) ? () => _controller.goBack() : null, + icon: const Icon(Icons.arrow_back_rounded), + ), + IconButton( + tooltip: '前进', + onPressed: (!_showHome && _canGoForward) ? () => _controller.goForward() : null, + icon: const Icon(Icons.arrow_forward_rounded), + ), + IconButton( + tooltip: '回到主页', + onPressed: _showHome ? null : _goHome, + icon: const Icon(Icons.home_rounded), + ), + ], + ), + ), + Expanded( + child: Stack( + children: [ + Positioned.fill( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 180), + child: _showHome ? _buildHomeView() : Webview(_controller), + ), + ), + if (_isLoading && !_showHome) + const Positioned.fill( + child: IgnorePointer( + child: Center(child: CircularProgressIndicator()), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildHomeView() { + final filteredGames = _games.where((game) { + if (_typeFilter != 'all' && game.gameType != _typeFilter) { + return false; + } + if (_searchQuery.isEmpty) { + return true; + } + final haystack = '${game.id} ${game.name} ${game.publisher} ${game.description}'.toLowerCase(); + return haystack.contains(_searchQuery); + }).toList(); + + return Container( + key: const ValueKey('home-view'), + color: const Color(0xFF0B0D14), + child: Column( + children: [ + Container( + margin: const EdgeInsets.fromLTRB(16, 16, 16, 10), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: const Color(0xFF151E2E), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0x223F628D)), + ), + child: Row( + children: [ + const Icon(Icons.sports_esports_rounded, color: Color(0xFF71D4FF)), + const SizedBox(width: 10), + const Expanded( + child: Text( + '已连接小游戏大厅。这里展示服务端已发布的 H5 小游戏。', + style: TextStyle(fontSize: 13, color: Color(0xFFD6E5F7)), + ), + ), + FilledButton.tonalIcon( + onPressed: _gamesLoading ? null : _loadMiniGames, + icon: const Icon(Icons.refresh_rounded), + label: const Text('刷新'), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: '搜索 ID / 名称 / 发布者', + prefixIcon: const Icon(Icons.search_rounded), + filled: true, + fillColor: const Color(0xFF151E2E), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide(color: Color(0x223F628D)), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide(color: Color(0x223F628D)), + ), + ), + ), + ), + const SizedBox(width: 12), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: const Color(0xFF151E2E), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: const Color(0x223F628D)), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: _typeFilter, + dropdownColor: const Color(0xFF151E2E), + items: const [ + DropdownMenuItem(value: 'all', child: Text('全部类型')), + DropdownMenuItem(value: 'official', child: Text('官方')), + DropdownMenuItem(value: 'community', child: Text('社区')), + ], + onChanged: (value) { + if (value == null) { + return; + } + setState(() { + _typeFilter = value; + }); + }, + ), + ), + ), + ], + ), + ), + Expanded( + child: Builder( + builder: (context) { + if (_gamesLoading) { + return const Center(child: CircularProgressIndicator()); + } + if (_homeError.isNotEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.cloud_off_rounded, size: 56, color: Color(0xFF6E8BA9)), + const SizedBox(height: 16), + Text(_homeError, textAlign: TextAlign.center, style: const TextStyle(color: Color(0xFFC7D6EA))), + const SizedBox(height: 16), + FilledButton(onPressed: _loadMiniGames, child: const Text('重新尝试')), + ], + ), + ), + ); + } + if (_games.isEmpty) { + return const Center( + child: Text('当前没有已发布的小游戏', style: TextStyle(color: Color(0xFFAFC2DE))), + ); + } + if (filteredGames.isEmpty) { + return const Center( + child: Text('没有符合条件的小游戏', style: TextStyle(color: Color(0xFFAFC2DE))), + ); + } + return ListView.separated( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + itemCount: filteredGames.length, + separatorBuilder: (_, _) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final game = filteredGames[index]; + return _MiniGameCard( + game: game, + onPlay: () => _openGame(game), + ); + }, + ); + }, + ), + ), + ], + ), + ); + } +} + +class _MiniGameCard extends StatelessWidget { + const _MiniGameCard({required this.game, required this.onPlay}); + + final MiniGameRecord game; + final VoidCallback onPlay; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: const Color(0xFF151E2E), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0x223F628D)), + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: onPlay, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Container( + width: 68, + height: 68, + decoration: BoxDecoration( + color: const Color(0xFF20314A), + borderRadius: BorderRadius.circular(16), + ), + child: const Icon(Icons.extension_rounded, size: 34, color: Color(0xFF7ED3FF)), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(game.name, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700)), + const SizedBox(height: 6), + Text( + game.description.isEmpty ? '暂无简介' : game.description, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 12, color: Color(0xFFAFC2DE)), + ), + const SizedBox(height: 10), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _MetaChip(label: 'ID ${game.id}'), + _MetaChip(label: game.gameType == 'community' ? '社区' : '官方'), + _MetaChip(label: game.publisher.isEmpty ? '发布者未填写' : game.publisher), + _MetaChip(label: game.publishDate.isEmpty ? '未设置发布时间' : game.publishDate), + ], + ), + ], + ), + ), + const SizedBox(width: 16), + FilledButton.icon( + onPressed: onPlay, + icon: const Icon(Icons.play_arrow_rounded), + label: const Text('开玩'), + ), + ], + ), + ), + ), + ), + ); + } +} + +class _MetaChip extends StatelessWidget { + const _MetaChip({required this.label}); + + final String label; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: const Color(0xFF20314A), + borderRadius: BorderRadius.circular(999), + ), + child: Text(label, style: const TextStyle(fontSize: 11, color: Color(0xFFD6E5F7))), + ); + } +} diff --git a/fdGamer/pubspec.lock b/fdGamer/pubspec.lock new file mode 100644 index 00000000..6f8fef5b --- /dev/null +++ b/fdGamer/pubspec.lock @@ -0,0 +1,284 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.19.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.9" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.3" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.11.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.flutter-io.cn" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.17.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.8" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" + screen_retriever_linux: + dependency: transitive + description: + name: screen_retriever_linux + sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" + screen_retriever_macos: + dependency: transitive + description: + name: screen_retriever_macos + sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" + screen_retriever_platform_interface: + dependency: transitive + description: + name: screen_retriever_platform_interface + sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" + screen_retriever_windows: + dependency: transitive + description: + name: screen_retriever_windows + sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.7" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "046d3928e16fa4dc46e8350415661755ab759d9fc97fc21b5ab295f71e4f0499" + url: "https://pub.flutter-io.cn" + source: hosted + version: "15.1.0" + webview_windows: + dependency: "direct main" + description: + path: "third_party/webview_windows" + relative: true + source: path + version: "0.4.0" + window_manager: + dependency: "direct main" + description: + name: window_manager + sha256: "7eb6d6c4164ec08e1bf978d6e733f3cebe792e2a23fb07cbca25c2872bfdbdcd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.5.1" +sdks: + dart: ">=3.10.4 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/fdGamer/pubspec.yaml b/fdGamer/pubspec.yaml new file mode 100644 index 00000000..b8cef28e --- /dev/null +++ b/fdGamer/pubspec.yaml @@ -0,0 +1,101 @@ +name: floating_mini_games +description: "独立小游戏托管窗口。" +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.10.4 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + webview_windows: ^0.4.0 + window_manager: ^0.5.1 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^6.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + assets: + - assets/bansonic icon.png + fonts: + - family: Bansonic + fonts: + - asset: assets/font.otf + +dependency_overrides: + webview_windows: + path: third_party/webview_windows + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/fdGamer/quick start.bat b/fdGamer/quick start.bat new file mode 100644 index 00000000..cc7e5ce4 --- /dev/null +++ b/fdGamer/quick start.bat @@ -0,0 +1,29 @@ +@echo off +setlocal +title 浮动小游戏 - 调试启动 + +set FLUTTER_BIN=E:\mcz_transform_flutter\flutter_windows_3.38.5-stable\flutter\bin +set PUB_HOSTED_URL=https://pub.flutter-io.cn +set FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn +set PATH=%FLUTTER_BIN%;%PATH% + +cd /d %~dp0 + +echo [fdGamer] Flutter SDK: %FLUTTER_BIN% +echo [fdGamer] Running flutter pub get... +call flutter.bat pub get +if errorlevel 1 goto :fail + +echo [fdGamer] Starting Windows debug session... +call flutter.bat run -d windows -- --server-base=http://127.0.0.1:8080 %* +if errorlevel 1 goto :fail + +goto :end + +:fail +echo [fdGamer] Failed. +pause +exit /b 1 + +:end +endlocal diff --git a/fdGamer/test/widget_test.dart b/fdGamer/test/widget_test.dart new file mode 100644 index 00000000..651b4fac --- /dev/null +++ b/fdGamer/test/widget_test.dart @@ -0,0 +1,14 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:floating_mini_games/main.dart'; + +void main() { + testWidgets('floating mini games app boots', (tester) async { + await tester.pumpWidget( + const FloatingMiniGamesApp( + config: AppLaunchConfig(serverBase: 'http://127.0.0.1:8080'), + ), + ); + expect(find.text('小游戏大厅'), findsOneWidget); + }); +} diff --git a/fdGamer/third_party/webview_windows/CHANGELOG.md b/fdGamer/third_party/webview_windows/CHANGELOG.md new file mode 100644 index 00000000..daa07117 --- /dev/null +++ b/fdGamer/third_party/webview_windows/CHANGELOG.md @@ -0,0 +1,136 @@ +## 0.4.0 + +* Enable MSVC coroutine support ([#278](https://github.com/jnschulze/flutter-webview-windows/pull/278)) +* Enable scrolling with trackpad ([#274](https://github.com/jnschulze/flutter-webview-windows/pull/274)) + +## 0.3.0 + +* Add full-screen support ([#189](https://github.com/jnschulze/flutter-webview-windows/pull/189)) +* Make `loadingState` a broadcast stream ([#193](https://github.com/jnschulze/flutter-webview-windows/pull/193)) +* Add `getWebViewVersion()` method ([#197](https://github.com/jnschulze/flutter-webview-windows/pull/197)) +* Fix string casting of paths that may contain Unicode characters ([#199](https://github.com/jnschulze/flutter-webview-windows/pull/199)) +* Add high-DPI screen support ([#203](https://github.com/jnschulze/flutter-webview-windows/pull/203)) +* Add `setZoomFactor` ([#214](https://github.com/jnschulze/flutter-webview-windows/pull/214)) +* Fix example ([#215](https://github.com/jnschulze/flutter-webview-windows/pull/215)) +* Fix Visual Studio 17.6 builds ([#252](https://github.com/jnschulze/flutter-webview-windows/pull/252)) + +## 0.2.2 + +* Remove `libfmt` dependency in favor of C++20 `std::format` +* Enable D3D texture bridge by default +* Make `executeScript` return the script's result + +## 0.2.1 + +* Add `WebviewController.addScriptToExecuteOnDocumentCreated` and `WebviewController.removeScriptToExecuteOnDocumentCreated` +* Add `WebviewController.onLoadError` stream +* Change `WebviewController.webMessage` stream type from `Map` to `dynamic` +* Add virtual hostname mapping support +* Add multi-touch support + +## 0.2.0 + +* Fix Flutter 3.0 null safety warning in example +* Bump WebView2 SDK version to `1.0.1210.3` +* Add an option for limiting the FPS +* Change data directory base path from `RoamingAppData` to `LocalAppData` + +## 0.1.9 + +* Fix Flutter 3.0 compatibility + +## 0.1.8 + +* Prefix CMake build target names to prevent collisions with other plugins + +## 0.1.7 + +* Add method for opening DevTools +* Update `TextureBridgeGpu` +* Update `libfmt` dependency + +## 0.1.7-dev.2 + +* Ensure Flutter apps referencing `webview_windows` still work on Windows 8. + +## 0.1.7-dev.1 + +* Remove windowsapp.lib dependency + +## 0.1.6 + +* Improve WebView creation error handling + +## 0.1.5 + +* Fix a potential crash during WebView creation + +## 0.1.4 + +* Improve error handling for Webview environment creation + +## 0.1.3 + +* Stability fixes + +## 0.1.2 + +* Unregister method channel handlers upon WebView destruction + +## 0.1.1 + +* Fix unicode string conversion in ExecuteScript and LoadStringContent +* Load CoreMessaging.dll on demand + +## 0.1.0 + +* Fix a string conversion issue +* Add an option for controlling popup window behavior +* Update Microsoft.Web.WebView2 and Microsoft.Windows.ImplementationLibrary + +## 0.0.9 + +* Fix resizing issues +* Add preliminary GpuSurfaceTexture support + +## 0.0.8 + +* Don't rely on AVX2 support +* Add history controls +* Add suspend/resume support +* Add support for disabling cache, clearing cookies etc. + +## 0.0.7 + +* Add support for handling permission requests +* Allow setting the background color +* Automatically download nuget + +## 0.0.6 + +* Fix mousewheel event handling +* Make text selection work +* Add method for setting the user agent +* Add support for JavaScript injection +* Add support for JSON message passing between Dart and JS +* Fix WebView disposal + +## 0.0.5 + +* Fix input field focus issue + +## 0.0.4 + +* Minor cleanup + +## 0.0.3 + +* Add support for additional cursor types + +## 0.0.2 + +* Add support for loading string content + +## 0.0.1 + +* Initial release diff --git a/fdGamer/third_party/webview_windows/LICENSE b/fdGamer/third_party/webview_windows/LICENSE new file mode 100644 index 00000000..729ddf9a --- /dev/null +++ b/fdGamer/third_party/webview_windows/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2021 Niklas Schulze +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/fdGamer/third_party/webview_windows/README.md b/fdGamer/third_party/webview_windows/README.md new file mode 100644 index 00000000..dda87143 --- /dev/null +++ b/fdGamer/third_party/webview_windows/README.md @@ -0,0 +1,38 @@ +# webview_windows + +[![CI](https://github.com/jnschulze/flutter-webview-windows/actions/workflows/ci.yml/badge.svg)](https://github.com/jnschulze/flutter-webview-windows/actions/workflows/ci.yml) +[![Pub](https://img.shields.io/pub/v/webview_windows.svg)](https://pub.dartlang.org/packages/webview_windows) + +A [Flutter](https://flutter.dev/) WebView plugin for Windows built on [Microsoft Edge WebView2](https://docs.microsoft.com/en-us/microsoft-edge/webview2/). + + +### Target platform requirements +- [WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) + Before initializing the webview, call `getWebViewVersion()` to check whether the required **WebView2 Runtime** is installed or not on the current system. If `getWebViewVersion()` returns null, guide your user to install **WebView2 Runtime** from this [page](https://developer.microsoft.com/en-us/microsoft-edge/webview2/). +- Windows 10 1809+ + +### Development platform requirements +- Visual Studio 2019 or higher +- Windows 11 SDK (10.0.22000.194 or higher) +- (recommended) nuget.exe in your $PATH *(The makefile attempts to download nuget if it's not installed, however, this fallback might not work in China)* + +## Demo +![image](https://user-images.githubusercontent.com/720469/116823636-d8b9fe00-ab85-11eb-9f91-b7bc819615ed.png) + +https://user-images.githubusercontent.com/720469/116716747-66f08180-a9d8-11eb-86ca-63ad5c24f07b.mp4 + + + +## Limitations +This plugin provides seamless composition of web-based contents with other Flutter widgets by rendering off-screen. + +Unfortunately, [Microsoft Edge WebView2](https://docs.microsoft.com/en-us/microsoft-edge/webview2/) doesn't currently have an explicit API for offscreen rendering. +In order to still be able to obtain a pixel buffer upon rendering a new frame, this plugin currently relies on the `Windows.Graphics.Capture` API provided by Windows 10. +The downside is that older Windows versions aren't currently supported. + +Older Windows versions might still be targeted by using `BitBlt` for the time being. + +See: +- https://github.com/MicrosoftEdge/WebView2Feedback/issues/20 +- https://github.com/MicrosoftEdge/WebView2Feedback/issues/526 +- https://github.com/MicrosoftEdge/WebView2Feedback/issues/547 diff --git a/fdGamer/third_party/webview_windows/analysis_options.yaml b/fdGamer/third_party/webview_windows/analysis_options.yaml new file mode 100644 index 00000000..2c1167e2 --- /dev/null +++ b/fdGamer/third_party/webview_windows/analysis_options.yaml @@ -0,0 +1,61 @@ +# Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +# for details. All rights reserved. Use of this source code is governed by a +# BSD-style license that can be found in the LICENSE file. +# +# Google internally enforced rules. See README.md for more information, +# including a list of lints that are intentionally _not_ enforced. + +linter: + rules: + - always_declare_return_types + - always_require_non_null_named_parameters + - annotate_overrides + - avoid_init_to_null + - avoid_null_checks_in_equality_operators + - avoid_relative_lib_imports + - avoid_return_types_on_setters + - avoid_shadowing_type_parameters + - avoid_single_cascade_in_expression_statements + - avoid_types_as_parameter_names + - await_only_futures + - camel_case_extensions + - curly_braces_in_flow_control_structures + - empty_catches + - empty_constructor_bodies + - library_names + - library_prefixes + - no_duplicate_case_values + - null_closures + - omit_local_variable_types + - prefer_adjacent_string_concatenation + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_contains + - prefer_equal_for_default_values + - prefer_final_fields + - prefer_for_elements_to_map_fromIterable + - prefer_generic_function_type_aliases + - prefer_if_null_operators + - prefer_inlined_adds + - prefer_is_empty + - prefer_is_not_empty + - prefer_iterable_whereType + - prefer_single_quotes + - prefer_spread_collections + - recursive_getters + - slash_for_doc_comments + - sort_child_properties_last + - type_init_formals + - unawaited_futures + - unnecessary_brace_in_string_interps + - unnecessary_const + - unnecessary_getters_setters + - unnecessary_new + - unnecessary_null_in_if_null_operators + - unnecessary_this + - unrelated_type_equality_checks + - unsafe_html + - use_full_hex_values_for_flutter_colors + - use_function_type_syntax_for_parameters + - use_rethrow_when_possible + - valid_regexps \ No newline at end of file diff --git a/fdGamer/third_party/webview_windows/example/README.md b/fdGamer/third_party/webview_windows/example/README.md new file mode 100644 index 00000000..fa809493 --- /dev/null +++ b/fdGamer/third_party/webview_windows/example/README.md @@ -0,0 +1,16 @@ +# webview_windows_example + +Demonstrates how to use the webview_windows plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/fdGamer/third_party/webview_windows/example/lib/main.dart b/fdGamer/third_party/webview_windows/example/lib/main.dart new file mode 100644 index 00000000..baa3e873 --- /dev/null +++ b/fdGamer/third_party/webview_windows/example/lib/main.dart @@ -0,0 +1,233 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'dart:async'; + +import 'package:webview_windows/webview_windows.dart'; +import 'package:window_manager/window_manager.dart'; + +final navigatorKey = GlobalKey(); + +void main() async { + // For full-screen example + WidgetsFlutterBinding.ensureInitialized(); + await windowManager.ensureInitialized(); + + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp(navigatorKey: navigatorKey, home: ExampleBrowser()); + } +} + +class ExampleBrowser extends StatefulWidget { + @override + State createState() => _ExampleBrowser(); +} + +class _ExampleBrowser extends State { + final _controller = WebviewController(); + final _textController = TextEditingController(); + final List _subscriptions = []; + bool _isWebviewSuspended = false; + + @override + void initState() { + super.initState(); + initPlatformState(); + } + + Future initPlatformState() async { + // Optionally initialize the webview environment using + // a custom user data directory + // and/or a custom browser executable directory + // and/or custom chromium command line flags + //await WebviewController.initializeEnvironment( + // additionalArguments: '--show-fps-counter'); + + try { + await _controller.initialize(); + _subscriptions.add(_controller.url.listen((url) { + _textController.text = url; + })); + + _subscriptions + .add(_controller.containsFullScreenElementChanged.listen((flag) { + debugPrint('Contains fullscreen element: $flag'); + windowManager.setFullScreen(flag); + })); + + await _controller.setBackgroundColor(Colors.transparent); + await _controller.setPopupWindowPolicy(WebviewPopupWindowPolicy.deny); + await _controller.loadUrl('https://flutter.dev'); + + if (!mounted) return; + setState(() {}); + } on PlatformException catch (e) { + WidgetsBinding.instance.addPostFrameCallback((_) { + showDialog( + context: context, + builder: (_) => AlertDialog( + title: Text('Error'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Code: ${e.code}'), + Text('Message: ${e.message}'), + ], + ), + actions: [ + TextButton( + child: Text('Continue'), + onPressed: () { + Navigator.of(context).pop(); + }, + ) + ], + )); + }); + } + } + + Widget compositeView() { + if (!_controller.value.isInitialized) { + return const Text( + 'Not Initialized', + style: TextStyle( + fontSize: 24.0, + fontWeight: FontWeight.w900, + ), + ); + } else { + return Padding( + padding: EdgeInsets.all(20), + child: Column( + children: [ + Card( + elevation: 0, + child: Row(children: [ + Expanded( + child: TextField( + decoration: InputDecoration( + hintText: 'URL', + contentPadding: EdgeInsets.all(10.0), + ), + textAlignVertical: TextAlignVertical.center, + controller: _textController, + onSubmitted: (val) { + _controller.loadUrl(val); + }, + ), + ), + IconButton( + icon: Icon(Icons.refresh), + splashRadius: 20, + onPressed: () { + _controller.reload(); + }, + ), + IconButton( + icon: Icon(Icons.developer_mode), + tooltip: 'Open DevTools', + splashRadius: 20, + onPressed: () { + _controller.openDevTools(); + }, + ) + ]), + ), + Expanded( + child: Card( + color: Colors.transparent, + elevation: 0, + clipBehavior: Clip.antiAliasWithSaveLayer, + child: Stack( + children: [ + Webview( + _controller, + permissionRequested: _onPermissionRequested, + ), + StreamBuilder( + stream: _controller.loadingState, + builder: (context, snapshot) { + if (snapshot.hasData && + snapshot.data == LoadingState.loading) { + return LinearProgressIndicator(); + } else { + return SizedBox(); + } + }), + ], + ))), + ], + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + floatingActionButton: FloatingActionButton( + tooltip: _isWebviewSuspended ? 'Resume webview' : 'Suspend webview', + onPressed: () async { + if (_isWebviewSuspended) { + await _controller.resume(); + } else { + await _controller.suspend(); + } + setState(() { + _isWebviewSuspended = !_isWebviewSuspended; + }); + }, + child: Icon(_isWebviewSuspended ? Icons.play_arrow : Icons.pause), + ), + appBar: AppBar( + title: StreamBuilder( + stream: _controller.title, + builder: (context, snapshot) { + return Text( + snapshot.hasData ? snapshot.data! : 'WebView (Windows) Example'); + }, + )), + body: Center( + child: compositeView(), + ), + ); + } + + Future _onPermissionRequested( + String url, WebviewPermissionKind kind, bool isUserInitiated) async { + final decision = await showDialog( + context: navigatorKey.currentContext!, + builder: (BuildContext context) => AlertDialog( + title: const Text('WebView permission requested'), + content: Text('WebView has requested permission \'$kind\''), + actions: [ + TextButton( + onPressed: () => + Navigator.pop(context, WebviewPermissionDecision.deny), + child: const Text('Deny'), + ), + TextButton( + onPressed: () => + Navigator.pop(context, WebviewPermissionDecision.allow), + child: const Text('Allow'), + ), + ], + ), + ); + + return decision ?? WebviewPermissionDecision.none; + } + + @override + void dispose() { + _subscriptions.forEach((s) => s.cancel()); + _controller.dispose(); + super.dispose(); + } +} diff --git a/fdGamer/third_party/webview_windows/example/pubspec.yaml b/fdGamer/third_party/webview_windows/example/pubspec.yaml new file mode 100644 index 00000000..2ed82717 --- /dev/null +++ b/fdGamer/third_party/webview_windows/example/pubspec.yaml @@ -0,0 +1,22 @@ +name: webview_windows_example +description: Demonstrates how to use the webview_windows plugin. + +publish_to: 'none' + +environment: + sdk: ">=2.12.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + + webview_windows: + path: ../ + window_manager: ^0.2.7 + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/fdGamer/third_party/webview_windows/example/test/widget_test.dart b/fdGamer/third_party/webview_windows/example/test/widget_test.dart new file mode 100644 index 00000000..ab73b3a2 --- /dev/null +++ b/fdGamer/third_party/webview_windows/example/test/widget_test.dart @@ -0,0 +1 @@ +void main() {} diff --git a/fdGamer/third_party/webview_windows/example/windows/CMakeLists.txt b/fdGamer/third_party/webview_windows/example/windows/CMakeLists.txt new file mode 100644 index 00000000..a53e41c1 --- /dev/null +++ b/fdGamer/third_party/webview_windows/example/windows/CMakeLists.txt @@ -0,0 +1,98 @@ +cmake_minimum_required(VERSION 3.15) + +set(CMAKE_SYSTEM_VERSION 10.0.22000 CACHE STRING INTERNAL FORCE) + +project(webview_windows_example LANGUAGES CXX) + +set(BINARY_NAME "webview_windows_example") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Configure build options. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() + +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/fdGamer/third_party/webview_windows/example/windows/flutter/CMakeLists.txt b/fdGamer/third_party/webview_windows/example/windows/flutter/CMakeLists.txt new file mode 100644 index 00000000..b02c5485 --- /dev/null +++ b/fdGamer/third_party/webview_windows/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,103 @@ +cmake_minimum_required(VERSION 3.15) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/fdGamer/third_party/webview_windows/example/windows/runner/CMakeLists.txt b/fdGamer/third_party/webview_windows/example/windows/runner/CMakeLists.txt new file mode 100644 index 00000000..851cbf85 --- /dev/null +++ b/fdGamer/third_party/webview_windows/example/windows/runner/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.15) + +project(runner LANGUAGES CXX) + +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) +apply_standard_settings(${BINARY_NAME}) +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/fdGamer/third_party/webview_windows/example/windows/runner/Runner.rc b/fdGamer/third_party/webview_windows/example/windows/runner/Runner.rc new file mode 100644 index 00000000..296e898a --- /dev/null +++ b/fdGamer/third_party/webview_windows/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#ifdef FLUTTER_BUILD_NUMBER +#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#else +#define VERSION_AS_NUMBER 1,0,0 +#endif + +#ifdef FLUTTER_BUILD_NAME +#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "Demonstrates how to use the webview_windows plugin." "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "webview_windows_example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2021 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "webview_windows_example.exe" "\0" + VALUE "ProductName", "webview_windows_example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/fdGamer/third_party/webview_windows/example/windows/runner/flutter_window.cpp b/fdGamer/third_party/webview_windows/example/windows/runner/flutter_window.cpp new file mode 100644 index 00000000..b43b9095 --- /dev/null +++ b/fdGamer/third_party/webview_windows/example/windows/runner/flutter_window.cpp @@ -0,0 +1,61 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/fdGamer/third_party/webview_windows/example/windows/runner/flutter_window.h b/fdGamer/third_party/webview_windows/example/windows/runner/flutter_window.h new file mode 100644 index 00000000..6da0652f --- /dev/null +++ b/fdGamer/third_party/webview_windows/example/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/fdGamer/third_party/webview_windows/example/windows/runner/main.cpp b/fdGamer/third_party/webview_windows/example/windows/runner/main.cpp new file mode 100644 index 00000000..e3eac9f5 --- /dev/null +++ b/fdGamer/third_party/webview_windows/example/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"webview_windows_example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/fdGamer/third_party/webview_windows/example/windows/runner/resource.h b/fdGamer/third_party/webview_windows/example/windows/runner/resource.h new file mode 100644 index 00000000..66a65d1e --- /dev/null +++ b/fdGamer/third_party/webview_windows/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/fdGamer/third_party/webview_windows/example/windows/runner/resources/app_icon.ico b/fdGamer/third_party/webview_windows/example/windows/runner/resources/app_icon.ico new file mode 100644 index 00000000..c04e20ca Binary files /dev/null and b/fdGamer/third_party/webview_windows/example/windows/runner/resources/app_icon.ico differ diff --git a/fdGamer/third_party/webview_windows/example/windows/runner/runner.exe.manifest b/fdGamer/third_party/webview_windows/example/windows/runner/runner.exe.manifest new file mode 100644 index 00000000..c977c4a4 --- /dev/null +++ b/fdGamer/third_party/webview_windows/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/fdGamer/third_party/webview_windows/example/windows/runner/utils.cpp b/fdGamer/third_party/webview_windows/example/windows/runner/utils.cpp new file mode 100644 index 00000000..d19bdbbc --- /dev/null +++ b/fdGamer/third_party/webview_windows/example/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + if (target_length == 0) { + return std::string(); + } + std::string utf8_string; + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/fdGamer/third_party/webview_windows/example/windows/runner/utils.h b/fdGamer/third_party/webview_windows/example/windows/runner/utils.h new file mode 100644 index 00000000..3879d547 --- /dev/null +++ b/fdGamer/third_party/webview_windows/example/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/fdGamer/third_party/webview_windows/example/windows/runner/win32_window.cpp b/fdGamer/third_party/webview_windows/example/windows/runner/win32_window.cpp new file mode 100644 index 00000000..c10f08dc --- /dev/null +++ b/fdGamer/third_party/webview_windows/example/windows/runner/win32_window.cpp @@ -0,0 +1,245 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/fdGamer/third_party/webview_windows/example/windows/runner/win32_window.h b/fdGamer/third_party/webview_windows/example/windows/runner/win32_window.h new file mode 100644 index 00000000..17ba4311 --- /dev/null +++ b/fdGamer/third_party/webview_windows/example/windows/runner/win32_window.h @@ -0,0 +1,98 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/fdGamer/third_party/webview_windows/lib/src/cursor.dart b/fdGamer/third_party/webview_windows/lib/src/cursor.dart new file mode 100644 index 00000000..1ad57b65 --- /dev/null +++ b/fdGamer/third_party/webview_windows/lib/src/cursor.dart @@ -0,0 +1,43 @@ +import 'package:flutter/services.dart'; + +const Map _cursors = { + 'none': SystemMouseCursors.none, + 'basic': SystemMouseCursors.basic, + 'click': SystemMouseCursors.click, + 'forbidden': SystemMouseCursors.forbidden, + 'wait': SystemMouseCursors.wait, + 'progress': SystemMouseCursors.progress, + 'contextMenu': SystemMouseCursors.contextMenu, + 'help': SystemMouseCursors.help, + 'text': SystemMouseCursors.text, + 'verticalText': SystemMouseCursors.verticalText, + 'cell': SystemMouseCursors.cell, + 'precise': SystemMouseCursors.precise, + 'move': SystemMouseCursors.move, + 'grab': SystemMouseCursors.grab, + 'grabbing': SystemMouseCursors.grabbing, + 'noDrop': SystemMouseCursors.noDrop, + 'alias': SystemMouseCursors.alias, + 'copy': SystemMouseCursors.copy, + 'disappearing': SystemMouseCursors.disappearing, + 'allScroll': SystemMouseCursors.allScroll, + 'resizeLeftRight': SystemMouseCursors.resizeLeftRight, + 'resizeUpDown': SystemMouseCursors.resizeUpDown, + 'resizeUpLeftDownRight': SystemMouseCursors.resizeUpLeftDownRight, + 'resizeUpRightDownLeft': SystemMouseCursors.resizeUpRightDownLeft, + 'resizeUp': SystemMouseCursors.resizeUp, + 'resizeDown': SystemMouseCursors.resizeDown, + 'resizeLeft': SystemMouseCursors.resizeLeft, + 'resizeRight': SystemMouseCursors.resizeRight, + 'resizeUpLeft': SystemMouseCursors.resizeUpLeft, + 'resizeUpRight': SystemMouseCursors.resizeUpRight, + 'resizeDownLeft': SystemMouseCursors.resizeDownLeft, + 'resizeDownRight': SystemMouseCursors.resizeDownRight, + 'resizeColumn': SystemMouseCursors.resizeColumn, + 'resizeRow': SystemMouseCursors.resizeRow, + 'zoomIn': SystemMouseCursors.zoomIn, + 'zoomOut': SystemMouseCursors.zoomOut, +}; + +SystemMouseCursor getCursorByName(String name) => + _cursors[name] ?? SystemMouseCursors.basic; diff --git a/fdGamer/third_party/webview_windows/lib/src/enums.dart b/fdGamer/third_party/webview_windows/lib/src/enums.dart new file mode 100644 index 00000000..119f3201 --- /dev/null +++ b/fdGamer/third_party/webview_windows/lib/src/enums.dart @@ -0,0 +1,66 @@ +/// Loading state +// Order must match WebviewLoadingState (see webview.h) +enum LoadingState { none, loading, navigationCompleted } + +/// Pointer button type +// Order must match WebviewPointerButton (see webview.h) +enum PointerButton { none, primary, secondary, tertiary } + +/// Pointer Event kind +// Order must match WebviewPointerEventKind (see webview.h) +enum WebviewPointerEventKind { activate, down, enter, leave, up, update } + +/// Permission kind +// Order must match WebviewPermissionKind (see webview.h) +enum WebviewPermissionKind { + unknown, + microphone, + camera, + geoLocation, + notifications, + otherSensors, + clipboardRead +} + +enum WebviewPermissionDecision { none, allow, deny } + +/// The policy for popup requests. +/// +/// [allow] allows popups and will create new windows. +/// [deny] suppresses popups. +/// [sameWindow] displays popup contents in the current WebView. +enum WebviewPopupWindowPolicy { allow, deny, sameWindow } + +/// The kind of cross origin resource access for virtual hosts +/// +/// [deny] all cross origin requests are denied. +/// [allow] all cross origin requests are allowed. +/// [denyCors] sub resource cross origin requests are allowed, otherwise denied. +/// +/// For more detailed information, please refer to +/// [Microsofts](https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2#corewebview2_host_resource_access_kind) +/// documentation. +// Order must match WebviewHostResourceAccessKind (see webview.h) +enum WebviewHostResourceAccessKind { deny, allow, denyCors } + +enum WebErrorStatus { + WebErrorStatusUnknown, + WebErrorStatusCertificateCommonNameIsIncorrect, + WebErrorStatusCertificateExpired, + WebErrorStatusClientCertificateContainsErrors, + WebErrorStatusCertificateRevoked, + WebErrorStatusCertificateIsInvalid, + WebErrorStatusServerUnreachable, + WebErrorStatusTimeout, + WebErrorStatusErrorHTTPInvalidServerResponse, + WebErrorStatusConnectionAborted, + WebErrorStatusConnectionReset, + WebErrorStatusDisconnected, + WebErrorStatusCannotConnect, + WebErrorStatusHostNameNotResolved, + WebErrorStatusOperationCanceled, + WebErrorStatusRedirectFailed, + WebErrorStatusUnexpectedError, + WebErrorStatusValidAuthenticationCredentialsRequired, + WebErrorStatusValidProxyAuthenticationRequired, +} diff --git a/fdGamer/third_party/webview_windows/lib/src/webview.dart b/fdGamer/third_party/webview_windows/lib/src/webview.dart new file mode 100644 index 00000000..4b548103 --- /dev/null +++ b/fdGamer/third_party/webview_windows/lib/src/webview.dart @@ -0,0 +1,739 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:ui'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +import 'enums.dart'; +import 'cursor.dart'; + +class HistoryChanged { + final bool canGoBack; + final bool canGoForward; + const HistoryChanged(this.canGoBack, this.canGoForward); +} + +typedef PermissionRequestedDelegate + = FutureOr Function( + String url, WebviewPermissionKind permissionKind, bool isUserInitiated); + +typedef ScriptID = String; + +/// Attempts to translate a button constant such as [kPrimaryMouseButton] +/// to a [PointerButton] +PointerButton getButton(int value) { + switch (value) { + case kPrimaryMouseButton: + return PointerButton.primary; + case kSecondaryMouseButton: + return PointerButton.secondary; + case kTertiaryButton: + return PointerButton.tertiary; + default: + return PointerButton.none; + } +} + +const String _pluginChannelPrefix = 'io.jns.webview.win'; +const MethodChannel _pluginChannel = MethodChannel(_pluginChannelPrefix); + +class WebviewValue { + const WebviewValue({ + required this.isInitialized, + }); + + final bool isInitialized; + + WebviewValue copyWith({ + bool? isInitialized, + }) { + return WebviewValue( + isInitialized: isInitialized ?? this.isInitialized, + ); + } + + WebviewValue.uninitialized() + : this( + isInitialized: false, + ); +} + +/// Controls a WebView and provides streams for various change events. +class WebviewController extends ValueNotifier { + /// Explicitly initializes the underlying WebView environment + /// using an optional [browserExePath], an optional [userDataPath] + /// and optional Chromium command line arguments [additionalArguments]. + /// + /// The environment is shared between all WebviewController instances and + /// can be initialized only once. Initialization must take place before any + /// WebviewController is created/initialized. + /// + /// Throws [PlatformException] if the environment was initialized before. + static Future initializeEnvironment( + {String? userDataPath, + String? browserExePath, + String? additionalArguments}) async { + return _pluginChannel + .invokeMethod('initializeEnvironment', { + 'userDataPath': userDataPath, + 'browserExePath': browserExePath, + 'additionalArguments': additionalArguments + }); + } + + /// Get the browser version info including channel name if it is not the + /// WebView2 Runtime. + /// Returns [null] if the webview2 runtime is not installed. + static Future getWebViewVersion() async { + return _pluginChannel.invokeMethod('getWebViewVersion'); + } + + late Completer _creatingCompleter; + int _textureId = 0; + bool _isDisposed = false; + + Future get ready => _creatingCompleter.future; + + PermissionRequestedDelegate? _permissionRequested; + + late MethodChannel _methodChannel; + late EventChannel _eventChannel; + StreamSubscription? _eventStreamSubscription; + + final StreamController _urlStreamController = + StreamController(); + + /// A stream reflecting the current URL. + Stream get url => _urlStreamController.stream; + + final StreamController _loadingStateStreamController = + StreamController.broadcast(); + final StreamController _onLoadErrorStreamController = + StreamController(); + + /// A stream reflecting the current loading state. + Stream get loadingState => _loadingStateStreamController.stream; + + /// A stream reflecting the navigation error when navigation completed with an error. + Stream get onLoadError => _onLoadErrorStreamController.stream; + + final StreamController _historyChangedStreamController = + StreamController(); + + /// A stream reflecting the current history state. + Stream get historyChanged => + _historyChangedStreamController.stream; + + final StreamController _securityStateChangedStreamController = + StreamController(); + + /// A stream reflecting the current security state. + Stream get securityStateChanged => + _securityStateChangedStreamController.stream; + + final StreamController _titleStreamController = + StreamController(); + + /// A stream reflecting the current document title. + Stream get title => _titleStreamController.stream; + + final StreamController _cursorStreamController = + StreamController.broadcast(); + + /// A stream reflecting the current cursor style. + Stream get _cursor => _cursorStreamController.stream; + + final StreamController _webMessageStreamController = + StreamController(); + + Stream get webMessage => _webMessageStreamController.stream; + + final StreamController + _containsFullScreenElementChangedStreamController = + StreamController.broadcast(); + + /// A stream reflecting whether the document currently contains full-screen elements. + Stream get containsFullScreenElementChanged => + _containsFullScreenElementChangedStreamController.stream; + + WebviewController() : super(WebviewValue.uninitialized()); + + /// Initializes the underlying platform view. + Future initialize() async { + if (_isDisposed) { + return Future.value(); + } + _creatingCompleter = Completer(); + try { + final reply = + await _pluginChannel.invokeMapMethod('initialize'); + + _textureId = reply!['textureId']; + _methodChannel = MethodChannel('$_pluginChannelPrefix/$_textureId'); + _eventChannel = EventChannel('$_pluginChannelPrefix/$_textureId/events'); + _eventStreamSubscription = + _eventChannel.receiveBroadcastStream().listen((event) { + final map = event as Map; + switch (map['type']) { + case 'urlChanged': + _urlStreamController.add(map['value']); + break; + case 'onLoadError': + final value = WebErrorStatus.values[map['value']]; + _onLoadErrorStreamController.add(value); + break; + case 'loadingStateChanged': + final value = LoadingState.values[map['value']]; + _loadingStateStreamController.add(value); + break; + case 'historyChanged': + final value = HistoryChanged( + map['value']['canGoBack'], map['value']['canGoForward']); + _historyChangedStreamController.add(value); + break; + case 'securityStateChanged': + _securityStateChangedStreamController.add(map['value']); + break; + case 'titleChanged': + _titleStreamController.add(map['value']); + break; + case 'cursorChanged': + _cursorStreamController.add(getCursorByName(map['value'])); + break; + case 'webMessageReceived': + try { + final message = json.decode(map['value']); + _webMessageStreamController.add(message); + } catch (ex) { + _webMessageStreamController.addError(ex); + } + break; + case 'containsFullScreenElementChanged': + _containsFullScreenElementChangedStreamController.add(map['value']); + break; + } + }); + + _methodChannel.setMethodCallHandler((call) { + if (call.method == 'permissionRequested') { + return _onPermissionRequested( + call.arguments as Map); + } + + throw MissingPluginException('Unknown method ${call.method}'); + }); + + value = value.copyWith(isInitialized: true); + _creatingCompleter.complete(); + } on PlatformException catch (e) { + _creatingCompleter.completeError(e); + } + + return _creatingCompleter.future; + } + + Future _onPermissionRequested(Map args) async { + if (_permissionRequested == null) { + return null; + } + + final url = args['url'] as String?; + final permissionKindIndex = args['permissionKind'] as int?; + final isUserInitiated = args['isUserInitiated'] as bool?; + + if (url != null && permissionKindIndex != null && isUserInitiated != null) { + final permissionKind = WebviewPermissionKind.values[permissionKindIndex]; + final decision = + await _permissionRequested!(url, permissionKind, isUserInitiated); + + switch (decision) { + case WebviewPermissionDecision.allow: + return true; + case WebviewPermissionDecision.deny: + return false; + default: + return null; + } + } + + return null; + } + + @override + Future dispose() async { + await _creatingCompleter.future; + if (!_isDisposed) { + _isDisposed = true; + await _eventStreamSubscription?.cancel(); + await _pluginChannel.invokeMethod('dispose', _textureId); + } + super.dispose(); + } + + /// Loads the given [url]. + Future loadUrl(String url) async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + return _methodChannel.invokeMethod('loadUrl', url); + } + + /// Loads a document from the given string. + Future loadStringContent(String content) async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + return _methodChannel.invokeMethod('loadStringContent', content); + } + + /// Reloads the current document. + Future reload() async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + return _methodChannel.invokeMethod('reload'); + } + + /// Stops all navigations and pending resource fetches. + Future stop() async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + return _methodChannel.invokeMethod('stop'); + } + + /// Navigates the WebView to the previous page in the navigation history. + Future goBack() async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + return _methodChannel.invokeMethod('goBack'); + } + + /// Navigates the WebView to the next page in the navigation history. + Future goForward() async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + return _methodChannel.invokeMethod('goForward'); + } + + /// Adds the provided JavaScript [script] to a list of scripts that should be run after the global + /// object has been created, but before the HTML document has been parsed and before any + /// other script included by the HTML document is run. + /// + /// Returns a [ScriptID] on success which can be used for [removeScriptToExecuteOnDocumentCreated]. + /// + /// see https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2?view=webview2-1.0.1264.42#addscripttoexecuteondocumentcreated + Future addScriptToExecuteOnDocumentCreated(String script) async { + if (_isDisposed) { + return null; + } + assert(value.isInitialized); + return _methodChannel.invokeMethod( + 'addScriptToExecuteOnDocumentCreated', script); + } + + /// Removes the script identified by [scriptId] from the list of registered scripts. + /// + /// see https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2?view=webview2-1.0.1264.42#removescripttoexecuteondocumentcreated + Future removeScriptToExecuteOnDocumentCreated(ScriptID scriptId) async { + if (_isDisposed) { + return null; + } + assert(value.isInitialized); + return _methodChannel.invokeMethod( + 'removeScriptToExecuteOnDocumentCreated', scriptId); + } + + /// Runs the JavaScript [script] in the current top-level document rendered in + /// the WebView and returns its result. + /// + /// see https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2?view=webview2-1.0.1264.42#executescript + Future executeScript(String script) async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + + final data = await _methodChannel.invokeMethod('executeScript', script); + if (data == null) return null; + return jsonDecode(data as String); + } + + /// Posts the given JSON-formatted message to the current document. + Future postWebMessage(String message) async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + return _methodChannel.invokeMethod('postWebMessage', message); + } + + /// Sets the user agent value. + Future setUserAgent(String userAgent) async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + return _methodChannel.invokeMethod('setUserAgent', userAgent); + } + + /// Clears browser cookies. + Future clearCookies() async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + return _methodChannel.invokeMethod('clearCookies'); + } + + /// Clears browser cache. + Future clearCache() async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + return _methodChannel.invokeMethod('clearCache'); + } + + /// Toggles ignoring cache for each request. If true, cache will not be used. + Future setCacheDisabled(bool disabled) async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + return _methodChannel.invokeMethod('setCacheDisabled', disabled); + } + + /// Opens the Browser DevTools in a separate window + Future openDevTools() async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + return _methodChannel.invokeMethod('openDevTools'); + } + + /// Sets the background color to the provided [color]. + /// + /// Due to a limitation of the underlying WebView implementation, + /// semi-transparent values are not supported. + /// Any non-zero alpha value will be considered as opaque (0xff). + Future setBackgroundColor(Color color) async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + return _methodChannel.invokeMethod( + 'setBackgroundColor', color.value.toSigned(32)); + } + + /// Sets the zoom factor. + Future setZoomFactor(double zoomFactor) async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + return _methodChannel.invokeMethod('setZoomFactor', zoomFactor); + } + + /// Sets the [WebviewPopupWindowPolicy]. + Future setPopupWindowPolicy( + WebviewPopupWindowPolicy popupPolicy) async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + return _methodChannel.invokeMethod( + 'setPopupWindowPolicy', popupPolicy.index); + } + + /// Suspends the web view. + Future suspend() async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + return _methodChannel.invokeMethod('suspend'); + } + + /// Resumes the web view. + Future resume() async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + return _methodChannel.invokeMethod('resume'); + } + + /// Adds a Virtual Host Name Mapping. + /// + /// Please refer to + /// [Microsofts](https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2_3#setvirtualhostnametofoldermapping) + /// documentation for more details. + Future addVirtualHostNameMapping(String hostName, String folderPath, + WebviewHostResourceAccessKind accessKind) async { + if (_isDisposed) { + return; + } + + return _methodChannel.invokeMethod( + 'setVirtualHostNameMapping', [hostName, folderPath, accessKind.index]); + } + + /// Removes a Virtual Host Name Mapping. + /// + /// Please refer to + /// [Microsofts](https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2_3#clearvirtualhostnametofoldermapping) + /// documentation for more details. + Future removeVirtualHostNameMapping(String hostName) async { + if (_isDisposed) { + return; + } + return _methodChannel.invokeMethod('clearVirtualHostNameMapping', hostName); + } + + /// Limits the number of frames per second to the given value. + Future setFpsLimit([int? maxFps = 0]) async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + return _methodChannel.invokeMethod('setFpsLimit', maxFps); + } + + /// Sends a Pointer (Touch) update + Future _setPointerUpdate(WebviewPointerEventKind kind, int pointer, + Offset position, double size, double pressure) async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + return _methodChannel.invokeMethod('setPointerUpdate', + [pointer, kind.index, position.dx, position.dy, size, pressure]); + } + + /// Moves the virtual cursor to [position]. + Future _setCursorPos(Offset position) async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + return _methodChannel + .invokeMethod('setCursorPos', [position.dx, position.dy]); + } + + /// Indicates whether the specified [button] is currently down. + Future _setPointerButtonState(PointerButton button, bool isDown) async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + return _methodChannel.invokeMethod('setPointerButton', + {'button': button.index, 'isDown': isDown}); + } + + /// Sets the horizontal and vertical scroll delta. + Future _setScrollDelta(double dx, double dy) async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + return _methodChannel.invokeMethod('setScrollDelta', [dx, dy]); + } + + /// Sets the surface size to the provided [size]. + Future _setSize(Size size, double scaleFactor) async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + return _methodChannel + .invokeMethod('setSize', [size.width, size.height, scaleFactor]); + } +} + +class Webview extends StatefulWidget { + final WebviewController controller; + final PermissionRequestedDelegate? permissionRequested; + final double? width; + final double? height; + + /// An optional scale factor. Defaults to [FlutterView.devicePixelRatio] for + /// rendering in native resolution. + /// Setting this to 1.0 will disable high-DPI support. + /// This should only be needed to mimic old behavior before high-DPI support + /// was available. + final double? scaleFactor; + + /// The [FilterQuality] used for scaling the texture's contents. + /// Defaults to [FilterQuality.none] as this renders in native resolution + /// unless specifying a [scaleFactor]. + final FilterQuality filterQuality; + + const Webview(this.controller, + {this.width, + this.height, + this.permissionRequested, + this.scaleFactor, + this.filterQuality = FilterQuality.none}); + + @override + _WebviewState createState() => _WebviewState(); +} + +class _WebviewState extends State { + final GlobalKey _key = GlobalKey(); + final _downButtons = {}; + + PointerDeviceKind _pointerKind = PointerDeviceKind.unknown; + + MouseCursor _cursor = SystemMouseCursors.basic; + + WebviewController get _controller => widget.controller; + + StreamSubscription? _cursorSubscription; + + @override + void initState() { + super.initState(); + + // TODO: Refactor callback and event handling and + // remove this line + _controller._permissionRequested = widget.permissionRequested; + + // Report initial surface size + WidgetsBinding.instance.addPostFrameCallback((_) => _reportSurfaceSize()); + + _cursorSubscription = _controller._cursor.listen((cursor) { + setState(() { + _cursor = cursor; + }); + }); + } + + @override + Widget build(BuildContext context) { + return (widget.height != null && widget.width != null) + ? SizedBox( + key: _key, + width: widget.width, + height: widget.height, + child: _buildInner()) + : SizedBox.expand(key: _key, child: _buildInner()); + } + + Widget _buildInner() { + return NotificationListener( + onNotification: (notification) { + _reportSurfaceSize(); + return true; + }, + child: SizeChangedLayoutNotifier( + child: _controller.value.isInitialized + ? Listener( + onPointerHover: (ev) { + // ev.kind is for whatever reason not set to touch + // even on touch input + if (_pointerKind == PointerDeviceKind.touch) { + // Ignoring hover events on touch for now + return; + } + _controller._setCursorPos(ev.localPosition); + }, + onPointerDown: (ev) { + _pointerKind = ev.kind; + if (ev.kind == PointerDeviceKind.touch) { + _controller._setPointerUpdate( + WebviewPointerEventKind.down, + ev.pointer, + ev.localPosition, + ev.size, + ev.pressure); + return; + } + final button = getButton(ev.buttons); + _downButtons[ev.pointer] = button; + _controller._setPointerButtonState(button, true); + }, + onPointerUp: (ev) { + _pointerKind = ev.kind; + if (ev.kind == PointerDeviceKind.touch) { + _controller._setPointerUpdate( + WebviewPointerEventKind.up, + ev.pointer, + ev.localPosition, + ev.size, + ev.pressure); + return; + } + final button = _downButtons.remove(ev.pointer); + if (button != null) { + _controller._setPointerButtonState(button, false); + } + }, + onPointerCancel: (ev) { + _pointerKind = ev.kind; + final button = _downButtons.remove(ev.pointer); + if (button != null) { + _controller._setPointerButtonState(button, false); + } + }, + onPointerMove: (ev) { + _pointerKind = ev.kind; + if (ev.kind == PointerDeviceKind.touch) { + _controller._setPointerUpdate( + WebviewPointerEventKind.update, + ev.pointer, + ev.localPosition, + ev.size, + ev.pressure); + } else { + _controller._setCursorPos(ev.localPosition); + } + }, + onPointerSignal: (signal) { + if (signal is PointerScrollEvent) { + _controller._setScrollDelta( + -signal.scrollDelta.dx, -signal.scrollDelta.dy); + } + }, + onPointerPanZoomUpdate: (signal) { + _controller._setScrollDelta( + signal.panDelta.dx, signal.panDelta.dy); + }, + child: MouseRegion( + cursor: _cursor, + child: Texture( + textureId: _controller._textureId, + filterQuality: widget.filterQuality, + )), + ) + : const SizedBox())); + } + + void _reportSurfaceSize() async { + final box = _key.currentContext?.findRenderObject() as RenderBox?; + if (box != null) { + await _controller.ready; + unawaited(_controller._setSize( + box.size, widget.scaleFactor ?? window.devicePixelRatio)); + } + } + + @override + void dispose() { + super.dispose(); + _cursorSubscription?.cancel(); + } +} diff --git a/fdGamer/third_party/webview_windows/lib/webview_windows.dart b/fdGamer/third_party/webview_windows/lib/webview_windows.dart new file mode 100644 index 00000000..1c1d5505 --- /dev/null +++ b/fdGamer/third_party/webview_windows/lib/webview_windows.dart @@ -0,0 +1,2 @@ +export 'src/enums.dart'; +export 'src/webview.dart'; diff --git a/fdGamer/third_party/webview_windows/pubspec.yaml b/fdGamer/third_party/webview_windows/pubspec.yaml new file mode 100644 index 00000000..441a1027 --- /dev/null +++ b/fdGamer/third_party/webview_windows/pubspec.yaml @@ -0,0 +1,23 @@ +name: webview_windows +description: A WebView2-powered webview implementation for the Windows platform. +version: 0.4.0 +repository: https://github.com/jnschulze/flutter-webview-windows +homepage: https://jns.io + +environment: + sdk: ">=2.13.0 <3.0.0" + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + plugin: + platforms: + windows: + pluginClass: WebviewWindowsPlugin diff --git a/fdGamer/third_party/webview_windows/test/webview_windows_test.dart b/fdGamer/third_party/webview_windows/test/webview_windows_test.dart new file mode 100644 index 00000000..ab73b3a2 --- /dev/null +++ b/fdGamer/third_party/webview_windows/test/webview_windows_test.dart @@ -0,0 +1 @@ +void main() {} diff --git a/fdGamer/third_party/webview_windows/windows/CMakeLists.txt b/fdGamer/third_party/webview_windows/windows/CMakeLists.txt new file mode 100644 index 00000000..45e79ea8 --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/CMakeLists.txt @@ -0,0 +1,98 @@ +cmake_minimum_required(VERSION 3.15) +set(PROJECT_NAME "webview_windows") + +set(WIL_VERSION "1.0.220914.1") +set(WEBVIEW_VERSION "1.0.1210.39") + +message(VERBOSE "CMake system version is ${CMAKE_SYSTEM_VERSION} (using SDK ${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION})") + +project(${PROJECT_NAME} LANGUAGES CXX) + +# This value is used when generating builds using this plugin, so it must +# not be changed +set(PLUGIN_NAME "webview_windows_plugin") + +set(NUGET_URL https://dist.nuget.org/win-x86-commandline/v5.10.0/nuget.exe) +set(NUGET_SHA256 852b71cc8c8c2d40d09ea49d321ff56fd2397b9d6ea9f96e532530307bbbafd3) + +find_program(NUGET nuget) +set(LOCAL_NUGET "${CMAKE_SOURCE_DIR}/../tools/nuget.exe") +if(EXISTS ${LOCAL_NUGET}) + set(NUGET ${LOCAL_NUGET}) +endif() +if(NOT NUGET) + message(NOTICE "Nuget is not installed.") + set(NUGET ${CMAKE_BINARY_DIR}/nuget.exe) + if (NOT EXISTS ${NUGET}) + message(NOTICE "Attempting to download nuget.") + file(DOWNLOAD ${NUGET_URL} ${NUGET}) + endif() + + file(SHA256 ${NUGET} NUGET_DL_HASH) + if (NOT NUGET_DL_HASH STREQUAL NUGET_SHA256) + message(FATAL_ERROR "Integrity check for ${NUGET} failed.") + endif() +endif() + +add_custom_target(${PROJECT_NAME}_DEPENDENCIES_DOWNLOAD ALL) +add_custom_command( + TARGET ${PROJECT_NAME}_DEPENDENCIES_DOWNLOAD PRE_BUILD + COMMAND ${NUGET} install Microsoft.Windows.ImplementationLibrary -Version ${WIL_VERSION} -ExcludeVersion -OutputDirectory ${CMAKE_BINARY_DIR}/packages + COMMAND ${NUGET} install Microsoft.Web.WebView2 -Version ${WEBVIEW_VERSION} -ExcludeVersion -OutputDirectory ${CMAKE_BINARY_DIR}/packages +) + +add_library(${PLUGIN_NAME} SHARED + "webview_windows_plugin.cc" + "webview_platform.cc" + "webview.cc" + "webview_host.cc" + "webview_bridge.cc" + "texture_bridge.cc" + "graphics_context.cc" + "util/direct3d11.interop.cc" + "util/rohelper.cc" + "util/string_converter.cc" +) + +if(MSVC) + target_compile_options(${PLUGIN_NAME} PRIVATE "/await") +endif() + +if(NOT FLUTTER_WEBVIEW_WINDOWS_USE_TEXTURE_FALLBACK) + message(STATUS "Building with D3D texture support.") + target_compile_definitions("${PLUGIN_NAME}" PRIVATE + HAVE_FLUTTER_D3D_TEXTURE + ) + target_sources("${PLUGIN_NAME}" PRIVATE + "texture_bridge_gpu.cc" + ) +else() + message(STATUS "Building with fallback PixelBuffer texture.") + target_sources("${PLUGIN_NAME}" PRIVATE + "texture_bridge_fallback.cc" + "util/cpuid/cpuinfo.cc" + ) + # Enable AVX2 for pixel buffer conversions + if(MSVC) + target_compile_options(${PLUGIN_NAME} PRIVATE "/arch:AVX2" "/await") + endif() +endif() + +apply_standard_settings(${PLUGIN_NAME}) +target_compile_features(${PLUGIN_NAME} PUBLIC cxx_std_20) # For std::format support + +set_target_properties(${PLUGIN_NAME} PROPERTIES CXX_VISIBILITY_PRESET hidden) + +target_link_libraries(${PLUGIN_NAME} PRIVATE ${CMAKE_BINARY_DIR}/packages/Microsoft.Web.WebView2/build/native/Microsoft.Web.WebView2.targets) +target_link_libraries(${PLUGIN_NAME} PRIVATE ${CMAKE_BINARY_DIR}/packages/Microsoft.Windows.ImplementationLibrary/build/native/Microsoft.Windows.ImplementationLibrary.targets) + +target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) +target_include_directories(${PLUGIN_NAME} INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/include" +) + +target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin) + +set(webview_windows_bundled_libraries + PARENT_SCOPE +) diff --git a/fdGamer/third_party/webview_windows/windows/graphics_context.cc b/fdGamer/third_party/webview_windows/windows/graphics_context.cc new file mode 100644 index 00000000..ce871eb9 --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/graphics_context.cc @@ -0,0 +1,150 @@ +#include "graphics_context.h" + +#include "util/d3dutil.h" +#include "util/direct3d11.interop.h" + +GraphicsContext::GraphicsContext(rx::RoHelper* rohelper) : rohelper_(rohelper) { + device_ = CreateD3DDevice(); + if (!device_) { + return; + } + + device_->GetImmediateContext(device_context_.put()); + if (FAILED(util::CreateDirect3D11DeviceFromDXGIDevice( + device_.try_as().get(), + (IInspectable**)device_winrt_.put()))) { + return; + } + + valid_ = true; +} + +winrt::com_ptr +GraphicsContext::CreateCompositor() { + HSTRING className; + HSTRING_HEADER classNameHeader; + + if (FAILED(rohelper_->GetStringReference( + RuntimeClass_Windows_UI_Composition_Compositor, &className, + &classNameHeader))) { + return nullptr; + } + + winrt::com_ptr af; + if (FAILED(rohelper_->GetActivationFactory( + className, __uuidof(IActivationFactory), af.put_void()))) { + return nullptr; + } + + winrt::com_ptr compositor; + if (FAILED(af->ActivateInstance( + reinterpret_cast(compositor.put())))) { + return nullptr; + } + + return compositor; +} + +winrt::com_ptr +GraphicsContext::CreateGraphicsCaptureItemFromVisual( + ABI::Windows::UI::Composition::IVisual* visual) const { + HSTRING className; + HSTRING_HEADER classNameHeader; + + if (FAILED(rohelper_->GetStringReference( + RuntimeClass_Windows_Graphics_Capture_GraphicsCaptureItem, &className, + &classNameHeader))) { + return nullptr; + } + + ABI::Windows::Graphics::Capture::IGraphicsCaptureItemStatics* + capture_item_statics; + if (FAILED(rohelper_->GetActivationFactory( + className, + __uuidof( + ABI::Windows::Graphics::Capture::IGraphicsCaptureItemStatics), + (void**)&capture_item_statics))) { + return nullptr; + } + + winrt::com_ptr + capture_item; + if (FAILED( + capture_item_statics->CreateFromVisual(visual, capture_item.put()))) { + return nullptr; + } + + return capture_item; +} + +winrt::com_ptr +GraphicsContext::CreateCaptureFramePool( + ABI::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice* device, + ABI::Windows::Graphics::DirectX::DirectXPixelFormat pixelFormat, + INT32 numberOfBuffers, ABI::Windows::Graphics::SizeInt32 size) const { + HSTRING className; + HSTRING_HEADER classNameHeader; + + if (FAILED(rohelper_->GetStringReference( + RuntimeClass_Windows_Graphics_Capture_Direct3D11CaptureFramePool, + &className, &classNameHeader))) { + return nullptr; + } + + ABI::Windows::Graphics::Capture::IDirect3D11CaptureFramePoolStatics* + capture_frame_pool_statics; + if (FAILED(rohelper_->GetActivationFactory( + className, + __uuidof(ABI::Windows::Graphics::Capture:: + IDirect3D11CaptureFramePoolStatics), + (void**)&capture_frame_pool_statics))) { + return nullptr; + } + + winrt::com_ptr + capture_frame_pool; + + if (FAILED(capture_frame_pool_statics->Create(device, pixelFormat, + numberOfBuffers, size, + capture_frame_pool.put()))) { + return nullptr; + } + + return capture_frame_pool; +} + +winrt::com_ptr +GraphicsContext::CreateFreeThreadedCaptureFramePool( + ABI::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice* device, + ABI::Windows::Graphics::DirectX::DirectXPixelFormat pixelFormat, + INT32 numberOfBuffers, ABI::Windows::Graphics::SizeInt32 size) const { + HSTRING className; + HSTRING_HEADER classNameHeader; + + if (FAILED(rohelper_->GetStringReference( + RuntimeClass_Windows_Graphics_Capture_Direct3D11CaptureFramePool, + &className, &classNameHeader))) { + return nullptr; + } + + ABI::Windows::Graphics::Capture::IDirect3D11CaptureFramePoolStatics2* + capture_frame_pool_statics; + if (FAILED(rohelper_->GetActivationFactory( + className, + __uuidof(ABI::Windows::Graphics::Capture:: + IDirect3D11CaptureFramePoolStatics2), + (void**)&capture_frame_pool_statics))) { + return nullptr; + } + + winrt::com_ptr + capture_frame_pool; + + if (FAILED(capture_frame_pool_statics->CreateFreeThreaded( + device, pixelFormat, numberOfBuffers, size, + capture_frame_pool.put()))) { + return nullptr; + } + + return capture_frame_pool; +} diff --git a/fdGamer/third_party/webview_windows/windows/graphics_context.h b/fdGamer/third_party/webview_windows/windows/graphics_context.h new file mode 100644 index 00000000..19ad824c --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/graphics_context.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include + +#include "util/rohelper.h" + +class GraphicsContext { + public: + GraphicsContext(rx::RoHelper* rohelper); + + inline bool IsValid() const { return valid_; } + + ABI::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice* device() const { + return device_winrt_.get(); + } + ID3D11Device* d3d_device() const { return device_.get(); } + ID3D11DeviceContext* d3d_device_context() const { + return device_context_.get(); + } + + winrt::com_ptr CreateCompositor(); + + winrt::com_ptr + CreateGraphicsCaptureItemFromVisual( + ABI::Windows::UI::Composition::IVisual* visual) const; + + winrt::com_ptr + CreateCaptureFramePool( + ABI::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice* device, + ABI::Windows::Graphics::DirectX::DirectXPixelFormat pixelFormat, + INT32 numberOfBuffers, ABI::Windows::Graphics::SizeInt32 size) const; + + winrt::com_ptr + CreateFreeThreadedCaptureFramePool( + ABI::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice* device, + ABI::Windows::Graphics::DirectX::DirectXPixelFormat pixelFormat, + INT32 numberOfBuffers, ABI::Windows::Graphics::SizeInt32 size) const; + + private: + bool valid_ = false; + rx::RoHelper* rohelper_; + winrt::com_ptr + device_winrt_; + winrt::com_ptr device_{nullptr}; + winrt::com_ptr device_context_{nullptr}; +}; diff --git a/fdGamer/third_party/webview_windows/windows/include/webview_windows/webview_windows_plugin.h b/fdGamer/third_party/webview_windows/windows/include/webview_windows/webview_windows_plugin.h new file mode 100644 index 00000000..912d6d25 --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/include/webview_windows/webview_windows_plugin.h @@ -0,0 +1,23 @@ +#ifndef FLUTTER_PLUGIN_WEBVIEW_WINDOWS_PLUGIN_H_ +#define FLUTTER_PLUGIN_WEBVIEW_WINDOWS_PLUGIN_H_ + +#include + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport) +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +FLUTTER_PLUGIN_EXPORT void WebviewWindowsPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_WEBVIEW_WINDOWS_PLUGIN_H_ diff --git a/fdGamer/third_party/webview_windows/windows/texture_bridge.cc b/fdGamer/third_party/webview_windows/windows/texture_bridge.cc new file mode 100644 index 00000000..89ae6cdc --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/texture_bridge.cc @@ -0,0 +1,176 @@ +#include "texture_bridge.h" + +#include + +#include +#include +#include +#include + +#include "util/direct3d11.interop.h" + +namespace { +const int kNumBuffers = 1; +} // namespace + +TextureBridge::TextureBridge(GraphicsContext* graphics_context, + ABI::Windows::UI::Composition::IVisual* visual) + : graphics_context_(graphics_context) { + capture_item_ = + graphics_context_->CreateGraphicsCaptureItemFromVisual(visual); + assert(capture_item_); + + capture_item_->add_Closed( + Microsoft::WRL::Callback>( + [](ABI::Windows::Graphics::Capture::IGraphicsCaptureItem* item, + IInspectable* args) -> HRESULT { + std::cerr << "Capture item was closed." << std::endl; + return S_OK; + }) + .Get(), + &on_closed_token_); +} + +TextureBridge::~TextureBridge() { + const std::lock_guard lock(mutex_); + StopInternal(); + if (capture_item_) { + capture_item_->remove_Closed(on_closed_token_); + } +} + +bool TextureBridge::Start() { + const std::lock_guard lock(mutex_); + if (is_running_ || !capture_item_) { + return false; + } + + ABI::Windows::Graphics::SizeInt32 size; + capture_item_->get_Size(&size); + + frame_pool_ = graphics_context_->CreateCaptureFramePool( + graphics_context_->device(), + static_cast( + kPixelFormat), + kNumBuffers, size); + assert(frame_pool_); + + frame_pool_->add_FrameArrived( + Microsoft::WRL::Callback>( + [this](ABI::Windows::Graphics::Capture::IDirect3D11CaptureFramePool* + pool, + IInspectable* args) -> HRESULT { + OnFrameArrived(); + return S_OK; + }) + .Get(), + &on_frame_arrived_token_); + + if (FAILED(frame_pool_->CreateCaptureSession(capture_item_.get(), + capture_session_.put()))) { + std::cerr << "Creating capture session failed." << std::endl; + return false; + } + + if (SUCCEEDED(capture_session_->StartCapture())) { + is_running_ = true; + return true; + } + + return false; +} + +void TextureBridge::Stop() { + const std::lock_guard lock(mutex_); + StopInternal(); +} + +void TextureBridge::StopInternal() { + if (is_running_) { + is_running_ = false; + frame_pool_->remove_FrameArrived(on_frame_arrived_token_); + auto closable = + capture_session_.try_as(); + assert(closable); + closable->Close(); + capture_session_ = nullptr; + } +} + +void TextureBridge::OnFrameArrived() { + const std::lock_guard lock(mutex_); + if (!is_running_) { + return; + } + + bool has_frame = false; + + winrt::com_ptr + frame; + auto hr = frame_pool_->TryGetNextFrame(frame.put()); + if (SUCCEEDED(hr) && frame) { + winrt::com_ptr< + ABI::Windows::Graphics::DirectX::Direct3D11::IDirect3DSurface> + frame_surface; + + if (SUCCEEDED(frame->get_Surface(frame_surface.put()))) { + last_frame_ = + util::TryGetDXGIInterfaceFromObject(frame_surface); + has_frame = !ShouldDropFrame(); + } + } + + if (needs_update_) { + ABI::Windows::Graphics::SizeInt32 size; + capture_item_->get_Size(&size); + frame_pool_->Recreate( + graphics_context_->device(), + static_cast( + kPixelFormat), + kNumBuffers, size); + needs_update_ = false; + } + + if (has_frame && frame_available_) { + frame_available_(); + } +} + +bool TextureBridge::ShouldDropFrame() { + if (!frame_duration_.has_value()) { + return false; + } + auto now = std::chrono::high_resolution_clock::now(); + + bool should_drop_frame = false; + if (last_frame_timestamp_.has_value()) { + auto diff = std::chrono::duration_cast( + now - last_frame_timestamp_.value()); + should_drop_frame = diff < frame_duration_.value(); + } + + if (!should_drop_frame) { + last_frame_timestamp_ = now; + } + return should_drop_frame; +} + +void TextureBridge::NotifySurfaceSizeChanged() { + const std::lock_guard lock(mutex_); + needs_update_ = true; +} + +void TextureBridge::SetFpsLimit(std::optional max_fps) { + const std::lock_guard lock(mutex_); + auto value = max_fps.value_or(0); + if (value != 0) { + frame_duration_ = FrameDuration(1000.0 / value); + } else { + frame_duration_.reset(); + last_frame_timestamp_.reset(); + } +} diff --git a/fdGamer/third_party/webview_windows/windows/texture_bridge.h b/fdGamer/third_party/webview_windows/windows/texture_bridge.h new file mode 100644 index 00000000..e527d47e --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/texture_bridge.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +#include "graphics_context.h" + +typedef struct { + size_t width; + size_t height; +} Size; + +class TextureBridge { + public: + typedef std::function FrameAvailableCallback; + typedef std::function SurfaceSizeChangedCallback; + typedef std::chrono::duration FrameDuration; + + TextureBridge(GraphicsContext* graphics_context, + ABI::Windows::UI::Composition::IVisual* visual); + virtual ~TextureBridge(); + + bool Start(); + void Stop(); + + void SetOnFrameAvailable(FrameAvailableCallback callback) { + frame_available_ = std::move(callback); + } + + void SetOnSurfaceSizeChanged(SurfaceSizeChangedCallback callback) { + surface_size_changed_ = std::move(callback); + } + + void NotifySurfaceSizeChanged(); + void SetFpsLimit(std::optional max_fps); + + protected: + bool is_running_ = false; + + const GraphicsContext* graphics_context_; + std::mutex mutex_; + std::optional frame_duration_ = std::nullopt; + + FrameAvailableCallback frame_available_; + SurfaceSizeChangedCallback surface_size_changed_; + std::atomic needs_update_ = false; + winrt::com_ptr last_frame_; + std::optional + last_frame_timestamp_; + + winrt::com_ptr + capture_item_; + winrt::com_ptr + frame_pool_; + winrt::com_ptr + capture_session_; + + EventRegistrationToken on_closed_token_ = {}; + EventRegistrationToken on_frame_arrived_token_ = {}; + + virtual void StopInternal(); + void OnFrameArrived(); + bool ShouldDropFrame(); + + // corresponds to DXGI_FORMAT_B8G8R8A8_UNORM + static constexpr auto kPixelFormat = ABI::Windows::Graphics::DirectX:: + DirectXPixelFormat::DirectXPixelFormat_B8G8R8A8UIntNormalized; +}; diff --git a/fdGamer/third_party/webview_windows/windows/texture_bridge_fallback.cc b/fdGamer/third_party/webview_windows/windows/texture_bridge_fallback.cc new file mode 100644 index 00000000..d5731f7f --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/texture_bridge_fallback.cc @@ -0,0 +1,135 @@ +#include "texture_bridge_fallback.h" + +#include + +#include "util/direct3d11.interop.h" +#include "util/swizzle.h" + +TextureBridgeFallback::TextureBridgeFallback( + GraphicsContext* graphics_context, + ABI::Windows::UI::Composition::IVisual* visual) + : TextureBridge(graphics_context, visual) {} + +TextureBridgeFallback::~TextureBridgeFallback() { + const std::lock_guard lock(buffer_mutex_); +} + +void TextureBridgeFallback::ProcessFrame( + winrt::com_ptr src_texture) { + D3D11_TEXTURE2D_DESC desc; + src_texture->GetDesc(&desc); + + const auto width = desc.Width; + const auto height = desc.Height; + + bool is_exact_size; + EnsureStagingTexture(width, height, is_exact_size); + + auto device_context = graphics_context_->d3d_device_context(); + auto staging_texture = staging_texture_.get(); + + if (is_exact_size) { + device_context->CopyResource(staging_texture, src_texture.get()); + } else { + D3D11_BOX client_box; + client_box.top = 0; + client_box.left = 0; + client_box.right = width; + client_box.bottom = height; + client_box.front = 0; + client_box.back = 1; + device_context->CopySubresourceRegion(staging_texture, 0, 0, 0, 0, + src_texture.get(), 0, &client_box); + } + + D3D11_MAPPED_SUBRESOURCE mappedResource; + if (!SUCCEEDED(device_context->Map(staging_texture, 0, D3D11_MAP_READ, 0, + &mappedResource))) { + return; + } + + { + const std::lock_guard lock(buffer_mutex_); + if (!pixel_buffer_ || pixel_buffer_->width != width || + pixel_buffer_->height != height) { + if (!pixel_buffer_) { + pixel_buffer_ = std::make_unique(); + pixel_buffer_->release_context = &buffer_mutex_; + // Gets invoked after the FlutterDesktopPixelBuffer's + // backing buffer has been uploaded. + pixel_buffer_->release_callback = [](void* opaque) { + auto mutex = reinterpret_cast(opaque); + // Gets locked just before |CopyPixelBuffer| returns. + mutex->unlock(); + }; + } + pixel_buffer_->width = width; + pixel_buffer_->height = height; + const auto size = width * height * 4; + backing_pixel_buffer_.reset(new uint8_t[size]); + pixel_buffer_->buffer = backing_pixel_buffer_.get(); + } + + const auto src_pitch_in_pixels = mappedResource.RowPitch / 4; + RGBA_to_BGRA(reinterpret_cast(backing_pixel_buffer_.get()), + static_cast(mappedResource.pData), height, + src_pitch_in_pixels, width); + } + + device_context->Unmap(staging_texture, 0); +} + +void TextureBridgeFallback::EnsureStagingTexture(uint32_t width, + uint32_t height, + bool& is_exact_size) { + // Only recreate an existing texture if it's too small. + if (!staging_texture_ || staging_texture_size_.width < width || + staging_texture_size_.height < height) { + D3D11_TEXTURE2D_DESC dstDesc = {}; + dstDesc.ArraySize = 1; + dstDesc.MipLevels = 1; + dstDesc.BindFlags = 0; + dstDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + dstDesc.Format = static_cast(kPixelFormat); + dstDesc.Width = width; + dstDesc.Height = height; + dstDesc.MiscFlags = 0; + dstDesc.SampleDesc.Count = 1; + dstDesc.SampleDesc.Quality = 0; + dstDesc.Usage = D3D11_USAGE_STAGING; + + staging_texture_ = nullptr; + if (!SUCCEEDED(graphics_context_->d3d_device()->CreateTexture2D( + &dstDesc, nullptr, staging_texture_.put()))) { + std::cerr << "Creating dst texture failed" << std::endl; + return; + } + + staging_texture_size_ = {width, height}; + } + + is_exact_size = staging_texture_size_.width == width && + staging_texture_size_.height == height; +} + +const FlutterDesktopPixelBuffer* TextureBridgeFallback::CopyPixelBuffer( + size_t width, size_t height) { + const std::lock_guard lock(mutex_); + + if (!is_running_) { + return nullptr; + } + + if (last_frame_) { + ProcessFrame(last_frame_); + } + + auto buffer = pixel_buffer_.get(); + // Only lock the mutex if the buffer is not null + // (to ensure the release callback gets called) + if (buffer) { + // Gets unlocked in the FlutterDesktopPixelBuffer's release callback. + buffer_mutex_.lock(); + } + return buffer; +} diff --git a/fdGamer/third_party/webview_windows/windows/texture_bridge_fallback.h b/fdGamer/third_party/webview_windows/windows/texture_bridge_fallback.h new file mode 100644 index 00000000..05b50c5d --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/texture_bridge_fallback.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include + +#include "texture_bridge.h" + +class TextureBridgeFallback : public TextureBridge { + public: + TextureBridgeFallback(GraphicsContext* graphics_context, + ABI::Windows::UI::Composition::IVisual* visual); + ~TextureBridgeFallback() override; + + const FlutterDesktopPixelBuffer* CopyPixelBuffer(size_t width, size_t height); + + private: + Size staging_texture_size_ = {0, 0}; + winrt::com_ptr staging_texture_{nullptr}; + std::mutex buffer_mutex_; + std::unique_ptr backing_pixel_buffer_; + std::unique_ptr pixel_buffer_; + + void ProcessFrame(winrt::com_ptr src_texture); + void EnsureStagingTexture(uint32_t width, uint32_t height, + bool& is_exact_size); +}; diff --git a/fdGamer/third_party/webview_windows/windows/texture_bridge_gpu.cc b/fdGamer/third_party/webview_windows/windows/texture_bridge_gpu.cc new file mode 100644 index 00000000..b6044c1e --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/texture_bridge_gpu.cc @@ -0,0 +1,99 @@ +#include "texture_bridge_gpu.h" + +#include + +#include "util/direct3d11.interop.h" + +TextureBridgeGpu::TextureBridgeGpu( + GraphicsContext* graphics_context, + ABI::Windows::UI::Composition::IVisual* visual) + : TextureBridge(graphics_context, visual) { + surface_descriptor_.struct_size = sizeof(FlutterDesktopGpuSurfaceDescriptor); + surface_descriptor_.format = + kFlutterDesktopPixelFormatNone; // no format required for DXGI surfaces +} + +void TextureBridgeGpu::ProcessFrame( + winrt::com_ptr src_texture) { + D3D11_TEXTURE2D_DESC desc; + src_texture->GetDesc(&desc); + + const auto width = desc.Width; + const auto height = desc.Height; + + EnsureSurface(width, height); + + auto device_context = graphics_context_->d3d_device_context(); + + device_context->CopyResource(surface_.get(), src_texture.get()); + device_context->Flush(); +} + +void TextureBridgeGpu::EnsureSurface(uint32_t width, uint32_t height) { + if (!surface_ || surface_size_.width != width || + surface_size_.height != height) { + D3D11_TEXTURE2D_DESC dstDesc = {}; + dstDesc.ArraySize = 1; + dstDesc.MipLevels = 1; + dstDesc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE; + dstDesc.CPUAccessFlags = 0; + dstDesc.Format = static_cast(kPixelFormat); + dstDesc.Width = width; + dstDesc.Height = height; + dstDesc.MiscFlags = D3D11_RESOURCE_MISC_SHARED; + dstDesc.SampleDesc.Count = 1; + dstDesc.SampleDesc.Quality = 0; + dstDesc.Usage = D3D11_USAGE_DEFAULT; + + surface_ = nullptr; + if (!SUCCEEDED(graphics_context_->d3d_device()->CreateTexture2D( + &dstDesc, nullptr, surface_.put()))) { + std::cerr << "Creating intermediate texture failed" << std::endl; + return; + } + + HANDLE shared_handle; + surface_.try_as(dxgi_surface_); + assert(dxgi_surface_); + dxgi_surface_->GetSharedHandle(&shared_handle); + + surface_descriptor_.handle = shared_handle; + surface_descriptor_.width = surface_descriptor_.visible_width = width; + surface_descriptor_.height = surface_descriptor_.visible_height = height; + surface_descriptor_.release_context = surface_.get(); + surface_descriptor_.release_callback = [](void* release_context) { + auto texture = reinterpret_cast(release_context); + texture->Release(); + }; + + surface_size_ = {width, height}; + } +} + +const FlutterDesktopGpuSurfaceDescriptor* +TextureBridgeGpu::GetSurfaceDescriptor(size_t width, size_t height) { + const std::lock_guard lock(mutex_); + + if (!is_running_) { + return nullptr; + } + + if (last_frame_) { + ProcessFrame(last_frame_); + } + + if (surface_) { + // Gets released in the SurfaceDescriptor's release callback. + surface_->AddRef(); + } + + return &surface_descriptor_; +} + +void TextureBridgeGpu::StopInternal() { + TextureBridge::StopInternal(); + + // For some reason, the destination surface needs to be recreated upon + // resuming. Force |EnsureSurface| to create a new one by resetting it here. + surface_ = nullptr; +} diff --git a/fdGamer/third_party/webview_windows/windows/texture_bridge_gpu.h b/fdGamer/third_party/webview_windows/windows/texture_bridge_gpu.h new file mode 100644 index 00000000..0454c919 --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/texture_bridge_gpu.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include "texture_bridge.h" + +class TextureBridgeGpu : public TextureBridge { + public: + TextureBridgeGpu(GraphicsContext* graphics_context, + ABI::Windows::UI::Composition::IVisual* visual); + + const FlutterDesktopGpuSurfaceDescriptor* GetSurfaceDescriptor(size_t width, + size_t height); + + protected: + void StopInternal() override; + + private: + FlutterDesktopGpuSurfaceDescriptor surface_descriptor_ = {}; + Size surface_size_ = {0, 0}; + winrt::com_ptr surface_{nullptr}; + winrt::com_ptr dxgi_surface_; + + void ProcessFrame(winrt::com_ptr src_texture); + void EnsureSurface(uint32_t width, uint32_t height); +}; diff --git a/fdGamer/third_party/webview_windows/windows/util/composition.desktop.interop.h b/fdGamer/third_party/webview_windows/windows/util/composition.desktop.interop.h new file mode 100644 index 00000000..ccb2ea6d --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/util/composition.desktop.interop.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +namespace util { + +winrt::com_ptr +TryCreateDesktopWindowTarget( + const winrt::com_ptr& + compositor, + HWND window) { + namespace abi = ABI::Windows::UI::Composition::Desktop; + auto interop = compositor.try_as(); + + winrt::com_ptr target; + interop->CreateDesktopWindowTarget(window, true, target.put()); + return target; +} + +} // namespace util diff --git a/fdGamer/third_party/webview_windows/windows/util/cpuid/cpuinfo.cc b/fdGamer/third_party/webview_windows/windows/util/cpuid/cpuinfo.cc new file mode 100644 index 00000000..acee3437 --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/util/cpuid/cpuinfo.cc @@ -0,0 +1,80 @@ +#include "cpuinfo.h" + +#include "detail/cpuinfo_impl.h" + +#if defined(_MSC_VER) && (defined(__x86_64__) || defined(_M_X64)) +#include "detail/init_msvc_x86.h" +#else +#include "detail/init_unknown.hpp" +#endif + +namespace cpuid { + +cpuinfo::cpuinfo() : impl_(new impl) { init_cpuinfo(*impl_); } + +cpuinfo::~cpuinfo() {} + +// x86 member functions +bool cpuinfo::has_fpu() const { return impl_->m_has_fpu; } + +bool cpuinfo::has_mmx() const { return impl_->m_has_mmx; } + +bool cpuinfo::has_sse() const { return impl_->m_has_sse; } + +bool cpuinfo::has_sse2() const { return impl_->m_has_sse2; } + +bool cpuinfo::has_sse3() const { return impl_->m_has_sse3; } + +bool cpuinfo::has_ssse3() const { return impl_->m_has_ssse3; } + +bool cpuinfo::has_sse4_1() const { return impl_->m_has_sse4_1; } + +bool cpuinfo::has_sse4_2() const { return impl_->m_has_sse4_2; } + +bool cpuinfo::has_pclmulqdq() const { return impl_->m_has_pclmulqdq; } + +bool cpuinfo::has_avx() const { return impl_->m_has_avx; } + +bool cpuinfo::has_avx2() const { return impl_->m_has_avx2; } + +bool cpuinfo::has_avx512_f() const { return impl_->m_has_avx512_f; } + +bool cpuinfo::has_avx512_dq() const { return impl_->m_has_avx512_dq; } + +bool cpuinfo::has_avx512_ifma() const { return impl_->m_has_avx512_ifma; } + +bool cpuinfo::has_avx512_pf() const { return impl_->m_has_avx512_pf; } + +bool cpuinfo::has_avx512_er() const { return impl_->m_has_avx512_er; } + +bool cpuinfo::has_avx512_cd() const { return impl_->m_has_avx512_cd; } + +bool cpuinfo::has_avx512_bw() const { return impl_->m_has_avx512_bw; } + +bool cpuinfo::has_avx512_vl() const { return impl_->m_has_avx512_vl; } + +bool cpuinfo::has_avx512_vbmi() const { return impl_->m_has_avx512_vbmi; } + +bool cpuinfo::has_avx512_vbmi2() const { return impl_->m_has_avx512_vbmi2; } + +bool cpuinfo::has_avx512_vnni() const { return impl_->m_has_avx512_vnni; } + +bool cpuinfo::has_avx512_bitalg() const { return impl_->m_has_avx512_bitalg; } + +bool cpuinfo::has_avx512_vpopcntdq() const { + return impl_->m_has_avx512_vpopcntdq; +} + +bool cpuinfo::has_avx512_4vnniw() const { return impl_->m_has_avx512_4vnniw; } + +bool cpuinfo::has_avx512_4fmaps() const { return impl_->m_has_avx512_4fmaps; } + +bool cpuinfo::has_avx512_vp2intersect() const { + return impl_->m_has_avx512_vp2intersect; +} + +bool cpuinfo::has_f16c() const { return impl_->m_has_f16c; } + +// ARM member functions +bool cpuinfo::has_neon() const { return impl_->m_has_neon; } +} // namespace cpuid diff --git a/fdGamer/third_party/webview_windows/windows/util/cpuid/cpuinfo.h b/fdGamer/third_party/webview_windows/windows/util/cpuid/cpuinfo.h new file mode 100644 index 00000000..0cee3598 --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/util/cpuid/cpuinfo.h @@ -0,0 +1,105 @@ +#pragma once + +#include + +namespace cpuid { + +class cpuinfo { + public: + struct impl; + + cpuinfo(); + ~cpuinfo(); + + // Has X87 FPU + bool has_fpu() const; + + // Return true if the CPU supports MMX + bool has_mmx() const; + + // Return true if the CPU supports SSE + bool has_sse() const; + + // Return true if the CPU supports SSE2 + bool has_sse2() const; + + // Return true if the CPU supports SSE3 + bool has_sse3() const; + + // Return true if the CPU supports SSSE3 + bool has_ssse3() const; + + // Return true if the CPU supports SSE 4.1 + bool has_sse4_1() const; + + // Return true if the CPU supports SSE 4.2 + bool has_sse4_2() const; + + // Return true if the CPU supports pclmulqdq + bool has_pclmulqdq() const; + + // Return true if the CPU supports AVX + bool has_avx() const; + + // Return true if the CPU supports AVX2 + bool has_avx2() const; + + // Return true if the CPU supports AVX512F + bool has_avx512_f() const; + + // Return true if the CPU supports AVX512DQ + bool has_avx512_dq() const; + + // Return true if the CPU supports AVX512_IFMA + bool has_avx512_ifma() const; + + // Return true if the CPU supports AVX512PF + bool has_avx512_pf() const; + + // Return true if the CPU supports AVX512ER + bool has_avx512_er() const; + + // Return true if the CPU supports AVX512CD + bool has_avx512_cd() const; + + // Return true if the CPU supports AVX512BW + bool has_avx512_bw() const; + + // Return true if the CPU supports AVX512VL + bool has_avx512_vl() const; + + // Return true if the CPU supports AVX512_VBMI + bool has_avx512_vbmi() const; + + // Return true if the CPU supports AVX512_VBMI2 + bool has_avx512_vbmi2() const; + + // Return true if the CPU supports AVX512_VNNI + bool has_avx512_vnni() const; + + // Return true if the CPU supports AVX512_BITALG + bool has_avx512_bitalg() const; + + // Return true if the CPU supports AVX512_VPOPCNTDQ + bool has_avx512_vpopcntdq() const; + + // Return true if the CPU supports AVX512_4VNNIW + bool has_avx512_4vnniw() const; + + // Return true if the CPU supports AVX512_4FMAPS + bool has_avx512_4fmaps() const; + + // Return true if the CPU supports AVX512_VP2INTERSECT + bool has_avx512_vp2intersect() const; + + // Return true if the CPU supports F16C + bool has_f16c() const; + + // Return true if the CPU supports NEON + bool has_neon() const; + + private: + // Private implementation + std::unique_ptr impl_; +}; +} // namespace cpuid diff --git a/fdGamer/third_party/webview_windows/windows/util/cpuid/detail/cpuinfo_impl.h b/fdGamer/third_party/webview_windows/windows/util/cpuid/detail/cpuinfo_impl.h new file mode 100644 index 00000000..1d2ee69f --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/util/cpuid/detail/cpuinfo_impl.h @@ -0,0 +1,69 @@ +#pragma once + +#include "../cpuinfo.h" + +namespace cpuid { + +struct cpuinfo::impl { + impl() + : m_has_fpu(false), + m_has_mmx(false), + m_has_sse(false), + m_has_sse2(false), + m_has_sse3(false), + m_has_ssse3(false), + m_has_sse4_1(false), + m_has_sse4_2(false), + m_has_pclmulqdq(false), + m_has_avx(false), + m_has_avx2(false), + m_has_avx512_f(false), + m_has_avx512_dq(false), + m_has_avx512_ifma(false), + m_has_avx512_pf(false), + m_has_avx512_er(false), + m_has_avx512_cd(false), + m_has_avx512_bw(false), + m_has_avx512_vl(false), + m_has_avx512_vbmi(false), + m_has_avx512_vbmi2(false), + m_has_avx512_vnni(false), + m_has_avx512_bitalg(false), + m_has_avx512_vpopcntdq(false), + m_has_avx512_4vnniw(false), + m_has_avx512_4fmaps(false), + m_has_avx512_vp2intersect(false), + m_has_f16c(false), + m_has_neon(false) {} + + bool m_has_fpu; + bool m_has_mmx; + bool m_has_sse; + bool m_has_sse2; + bool m_has_sse3; + bool m_has_ssse3; + bool m_has_sse4_1; + bool m_has_sse4_2; + bool m_has_pclmulqdq; + bool m_has_avx; + bool m_has_avx2; + bool m_has_avx512_f; + bool m_has_avx512_dq; + bool m_has_avx512_ifma; + bool m_has_avx512_pf; + bool m_has_avx512_er; + bool m_has_avx512_cd; + bool m_has_avx512_bw; + bool m_has_avx512_vl; + bool m_has_avx512_vbmi; + bool m_has_avx512_vbmi2; + bool m_has_avx512_vnni; + bool m_has_avx512_bitalg; + bool m_has_avx512_vpopcntdq; + bool m_has_avx512_4vnniw; + bool m_has_avx512_4fmaps; + bool m_has_avx512_vp2intersect; + bool m_has_f16c; + bool m_has_neon; +}; +} // namespace cpuid diff --git a/fdGamer/third_party/webview_windows/windows/util/cpuid/detail/extract_x86_flags.h b/fdGamer/third_party/webview_windows/windows/util/cpuid/detail/extract_x86_flags.h new file mode 100644 index 00000000..5bea5866 --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/util/cpuid/detail/extract_x86_flags.h @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include "cpuinfo_impl.h" + +namespace cpuid { + +void extract_x86_flags(cpuinfo::impl& info, uint32_t ecx, uint32_t edx) { + info.m_has_fpu = (edx & (1 << 0)) != 0; + info.m_has_mmx = (edx & (1 << 23)) != 0; + info.m_has_sse = (edx & (1 << 25)) != 0; + info.m_has_sse2 = (edx & (1 << 26)) != 0; + info.m_has_sse3 = (ecx & (1 << 0)) != 0; + info.m_has_ssse3 = (ecx & (1 << 9)) != 0; + info.m_has_sse4_1 = (ecx & (1 << 19)) != 0; + info.m_has_sse4_2 = (ecx & (1 << 20)) != 0; + info.m_has_pclmulqdq = (ecx & (1 << 1)) != 0; + info.m_has_avx = (ecx & (1 << 28)) != 0; + info.m_has_f16c = (ecx & (1 << 29)) != 0; +} + +void extract_x86_extended_flags(cpuinfo::impl& info, uint32_t ebx, uint32_t ecx, + uint32_t edx) { + info.m_has_avx2 = (ebx & (1 << 5)) != 0; + info.m_has_avx512_f = (ebx & (1 << 16)) != 0; + info.m_has_avx512_dq = (ebx & (1 << 17)) != 0; + info.m_has_avx512_ifma = (ebx & (1 << 21)) != 0; + info.m_has_avx512_pf = (ebx & (1 << 26)) != 0; + info.m_has_avx512_er = (ebx & (1 << 27)) != 0; + info.m_has_avx512_cd = (ebx & (1 << 28)) != 0; + info.m_has_avx512_bw = (ebx & (1 << 30)) != 0; + info.m_has_avx512_vl = (ebx & (1 << 31)) != 0; + info.m_has_avx512_vbmi = (ecx & (1 << 1)) != 0; + info.m_has_avx512_vbmi2 = (ecx & (1 << 6)) != 0; + info.m_has_avx512_vnni = (ecx & (1 << 11)) != 0; + info.m_has_avx512_bitalg = (ecx & (1 << 12)) != 0; + info.m_has_avx512_vpopcntdq = (ecx & (1 << 14)) != 0; + info.m_has_avx512_4vnniw = (edx & (1 << 2)) != 0; + info.m_has_avx512_4fmaps = (edx & (1 << 3)) != 0; + info.m_has_avx512_vp2intersect = (edx & (1 << 8)) != 0; +} +} // namespace cpuid diff --git a/fdGamer/third_party/webview_windows/windows/util/cpuid/detail/init_msvc_x86.h b/fdGamer/third_party/webview_windows/windows/util/cpuid/detail/init_msvc_x86.h new file mode 100644 index 00000000..697270b1 --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/util/cpuid/detail/init_msvc_x86.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include "cpuinfo_impl.h" +#include "extract_x86_flags.h" + +namespace cpuid { + +void init_cpuinfo(cpuinfo::impl& info) { + int registers[4]; + + // The register information per input can be extracted from here: + // http://en.wikipedia.org/wiki/CPUID + // + // CPUID should be called with EAX=0 first, as this will return the + // maximum supported EAX input value for future calls + __cpuid(registers, 0); + uint32_t maximum_eax = registers[0]; + + // Set registers for basic flag extraction, eax=1 + // All CPUs should support index=1 + if (maximum_eax >= 1U) { + __cpuid(registers, 1); + extract_x86_flags(info, registers[2], registers[3]); + } + + // Set registers for extended flags extraction, eax=7 and ecx=0 + // This operation is not supported on older CPUs, so it should be skipped + // to avoid incorrect results + if (maximum_eax >= 7U) { + __cpuidex(registers, 7, 0); + extract_x86_extended_flags(info, registers[1], registers[2], registers[3]); + } +} +} // namespace cpuid diff --git a/fdGamer/third_party/webview_windows/windows/util/cpuid/detail/init_unknown.h b/fdGamer/third_party/webview_windows/windows/util/cpuid/detail/init_unknown.h new file mode 100644 index 00000000..3b52ef2b --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/util/cpuid/detail/init_unknown.h @@ -0,0 +1,9 @@ + +#pragma once + +#include "cpuinfo_impl.h" + +namespace cpuid { + +void init_cpuinfo(cpuinfo::impl& info) { (void)info; } +} // namespace cpuid diff --git a/fdGamer/third_party/webview_windows/windows/util/d3dutil.h b/fdGamer/third_party/webview_windows/windows/util/d3dutil.h new file mode 100644 index 00000000..a32bc157 --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/util/d3dutil.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include + +inline auto CreateD3DDevice(D3D_DRIVER_TYPE const type, + winrt::com_ptr& device) { + WINRT_ASSERT(!device); + + UINT flags = + D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_VIDEO_SUPPORT; + + //#ifdef _DEBUG + // flags |= D3D11_CREATE_DEVICE_DEBUG; + //#endif + + return D3D11CreateDevice(nullptr, type, nullptr, flags, nullptr, 0, + D3D11_SDK_VERSION, device.put(), nullptr, nullptr); +} + +inline auto CreateD3DDevice() { + winrt::com_ptr device; + HRESULT hr = CreateD3DDevice(D3D_DRIVER_TYPE_HARDWARE, device); + + if (DXGI_ERROR_UNSUPPORTED == hr) { + CreateD3DDevice(D3D_DRIVER_TYPE_WARP, device); + } + + return device; +} diff --git a/fdGamer/third_party/webview_windows/windows/util/direct3d11.interop.cc b/fdGamer/third_party/webview_windows/windows/util/direct3d11.interop.cc new file mode 100644 index 00000000..61ae5379 --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/util/direct3d11.interop.cc @@ -0,0 +1,43 @@ +#include "direct3d11.interop.h" + +namespace util { + +namespace { + +typedef HRESULT(WINAPI* CreateDirect3D11DeviceFromDXGIDeviceFn)(IDXGIDevice*, + LPVOID*); + +struct D3DFuncs { + CreateDirect3D11DeviceFromDXGIDeviceFn CreateDirect3D11DeviceFromDXGIDevice = + nullptr; + + D3DFuncs() { + auto handle = GetModuleHandle(L"d3d11.dll"); + if (!handle) { + return; + } + + CreateDirect3D11DeviceFromDXGIDevice = + reinterpret_cast( + GetProcAddress(handle, "CreateDirect3D11DeviceFromDXGIDevice")); + } + + static const D3DFuncs& instance() { + static D3DFuncs funcs; + return funcs; + } +}; + +} // namespace + +HRESULT CreateDirect3D11DeviceFromDXGIDevice(IDXGIDevice* dxgiDevice, + IInspectable** graphicsDevice) { + auto ptr = D3DFuncs::instance().CreateDirect3D11DeviceFromDXGIDevice; + if (ptr) { + return ptr(dxgiDevice, reinterpret_cast(graphicsDevice)); + } + + return E_NOTIMPL; +} + +} // namespace util diff --git a/fdGamer/third_party/webview_windows/windows/util/direct3d11.interop.h b/fdGamer/third_party/webview_windows/windows/util/direct3d11.interop.h new file mode 100644 index 00000000..a9cf7928 --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/util/direct3d11.interop.h @@ -0,0 +1,49 @@ + +#pragma once + +#include +#include +#include + +#include "dxgi.h" + +namespace Windows { +namespace Graphics { +namespace DirectX { +namespace Direct3D11 { +struct __declspec(uuid("A9B3D012-3DF2-4EE3-B8D1-8695F457D3C1")) + IDirect3DDxgiInterfaceAccess : ::IUnknown { + virtual HRESULT __stdcall GetInterface(GUID const& id, void** object) = 0; +}; + +} // namespace Direct3D11 +} // namespace DirectX +} // namespace Graphics +} // namespace Windows + +namespace util { + +HRESULT CreateDirect3D11DeviceFromDXGIDevice(IDXGIDevice* dxgiDevice, + IInspectable** graphicsDevice); + +template +auto GetDXGIInterfaceFromObject( + winrt::Windows::Foundation::IInspectable const& object) { + auto access = object.as< + Windows::Graphics::DirectX::Direct3D11::IDirect3DDxgiInterfaceAccess>(); + winrt::com_ptr result; + winrt::check_hresult( + access->GetInterface(winrt::guid_of(), result.put_void())); + return result; +} + +template +auto TryGetDXGIInterfaceFromObject(const winrt::com_ptr& object) { + auto access = object.try_as< + Windows::Graphics::DirectX::Direct3D11::IDirect3DDxgiInterfaceAccess>(); + winrt::com_ptr result; + access->GetInterface(winrt::guid_of(), result.put_void()); + return result; +} + +} // namespace util diff --git a/fdGamer/third_party/webview_windows/windows/util/rohelper.cc b/fdGamer/third_party/webview_windows/windows/util/rohelper.cc new file mode 100644 index 00000000..6fa6ab0b --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/util/rohelper.cc @@ -0,0 +1,244 @@ +// Based on ANGLE's RoHelper (CompositorNativeWindow11.{cpp,h}) +// - https://github.com/google/angle/blob/main/src/libANGLE/renderer/d3d/d3d11/converged/CompositorNativeWindow11.h +// - https://github.com/google/angle/blob/main/src/libANGLE/renderer/d3d/d3d11/converged/CompositorNativeWindow11.cpp +// - https://gist.github.com/clarkezone/43e984fb9bdcd2cfcd9a4f41c208a02f +// +// Copyright 2018 The ANGLE Project Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// +// Neither the name of TransGaming Inc., Google Inc., 3DLabs Inc. +// Ltd., nor the names of their contributors may be used to endorse +// or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +// COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#include "rohelper.h" + +#include +#include + +namespace rx { +template +bool AssignProcAddress(HMODULE comBaseModule, const char* name, T*& outProc) { + outProc = reinterpret_cast(GetProcAddress(comBaseModule, name)); + return *outProc != nullptr; +} + +RoHelper::RoHelper(RO_INIT_TYPE init_type) + : mFpWindowsCreateStringReference(nullptr), + mFpGetActivationFactory(nullptr), + mFpWindowsCompareStringOrdinal(nullptr), + mFpCreateDispatcherQueueController(nullptr), + mFpWindowsDeleteString(nullptr), + mFpRoInitialize(nullptr), + mFpRoUninitialize(nullptr), + mWinRtAvailable(false), + mComBaseModule(nullptr), + mCoreMessagingModule(nullptr) { +#ifdef WINUWP + mFpWindowsCreateStringReference = &::WindowsCreateStringReference; + mFpRoInitialize = &::RoInitialize; + mFpRoUninitialize = &::RoUninitialize; + mFpWindowsDeleteString = &::WindowsDeleteString; + mFpGetActivationFactory = &::RoGetActivationFactory; + mFpWindowsCompareStringOrdinal = &::WindowsCompareStringOrdinal; + mFpCreateDispatcherQueueController = &::CreateDispatcherQueueController; + mWinRtAvailable = true; +#else + + mComBaseModule = LoadLibraryA("ComBase.dll"); + + if (mComBaseModule == nullptr) { + return; + } + + if (!AssignProcAddress(mComBaseModule, "WindowsCreateStringReference", + mFpWindowsCreateStringReference)) { + return; + } + + if (!AssignProcAddress(mComBaseModule, "RoGetActivationFactory", + mFpGetActivationFactory)) { + return; + } + + if (!AssignProcAddress(mComBaseModule, "WindowsCompareStringOrdinal", + mFpWindowsCompareStringOrdinal)) { + return; + } + + if (!AssignProcAddress(mComBaseModule, "WindowsDeleteString", + mFpWindowsDeleteString)) { + return; + } + + if (!AssignProcAddress(mComBaseModule, "RoInitialize", mFpRoInitialize)) { + return; + } + + if (!AssignProcAddress(mComBaseModule, "RoUninitialize", mFpRoUninitialize)) { + return; + } + + mCoreMessagingModule = LoadLibraryA("coremessaging.dll"); + + if (mCoreMessagingModule == nullptr) { + return; + } + + if (!AssignProcAddress(mCoreMessagingModule, + "CreateDispatcherQueueController", + mFpCreateDispatcherQueueController)) { + return; + } + + auto result = RoInitialize(init_type); + + if (SUCCEEDED(result) || result == S_FALSE || result == RPC_E_CHANGED_MODE) { + mWinRtAvailable = true; + } +#endif +} + +RoHelper::~RoHelper() { +#ifndef WINUWP + if (mWinRtAvailable) { + RoUninitialize(); + } + + if (mCoreMessagingModule != nullptr) { + FreeLibrary(mCoreMessagingModule); + mCoreMessagingModule = nullptr; + } + + if (mComBaseModule != nullptr) { + FreeLibrary(mComBaseModule); + mComBaseModule = nullptr; + } +#endif +} + +bool RoHelper::WinRtAvailable() const { return mWinRtAvailable; } + +bool RoHelper::SupportedWindowsRelease() { + if (!mWinRtAvailable) { + return false; + } + + HSTRING className, contractName; + HSTRING_HEADER classNameHeader, contractNameHeader; + boolean isSupported = false; + + HRESULT hr = GetStringReference( + RuntimeClass_Windows_Foundation_Metadata_ApiInformation, &className, + &classNameHeader); + + if (FAILED(hr)) { + return !!isSupported; + } + + Microsoft::WRL::ComPtr< + ABI::Windows::Foundation::Metadata::IApiInformationStatics> + api; + + hr = GetActivationFactory( + className, + __uuidof(ABI::Windows::Foundation::Metadata::IApiInformationStatics), + &api); + + if (FAILED(hr)) { + return !!isSupported; + } + + hr = GetStringReference(L"Windows.Foundation.UniversalApiContract", + &contractName, &contractNameHeader); + if (FAILED(hr)) { + return !!isSupported; + } + + api->IsApiContractPresentByMajor(contractName, 6, &isSupported); + + return !!isSupported; +} + +HRESULT RoHelper::GetStringReference(PCWSTR source, HSTRING* act, + HSTRING_HEADER* header) { + if (!mWinRtAvailable) { + return E_FAIL; + } + + const wchar_t* str = static_cast(source); + + unsigned int length; + HRESULT hr = SizeTToUInt32(::wcslen(str), &length); + if (FAILED(hr)) { + return hr; + } + + return mFpWindowsCreateStringReference(source, length, header, act); +} + +HRESULT RoHelper::GetActivationFactory(const HSTRING act, + const IID& interfaceId, void** fac) { + if (!mWinRtAvailable) { + return E_FAIL; + } + auto hr = mFpGetActivationFactory(act, interfaceId, fac); + return hr; +} + +HRESULT RoHelper::WindowsCompareStringOrdinal(HSTRING one, HSTRING two, + int* result) { + if (!mWinRtAvailable) { + return E_FAIL; + } + return mFpWindowsCompareStringOrdinal(one, two, result); +} + +HRESULT RoHelper::CreateDispatcherQueueController( + DispatcherQueueOptions options, + ABI::Windows::System::IDispatcherQueueController** + dispatcherQueueController) { + if (!mWinRtAvailable) { + return E_FAIL; + } + return mFpCreateDispatcherQueueController(options, dispatcherQueueController); +} + +HRESULT RoHelper::WindowsDeleteString(HSTRING one) { + if (!mWinRtAvailable) { + return E_FAIL; + } + return mFpWindowsDeleteString(one); +} + +HRESULT RoHelper::RoInitialize(RO_INIT_TYPE type) { + return mFpRoInitialize(type); +} + +void RoHelper::RoUninitialize() { mFpRoUninitialize(); } +} // namespace rx diff --git a/fdGamer/third_party/webview_windows/windows/util/rohelper.h b/fdGamer/third_party/webview_windows/windows/util/rohelper.h new file mode 100644 index 00000000..12e7e6cc --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/util/rohelper.h @@ -0,0 +1,97 @@ +// Based on ANGLE's RoHelper (CompositorNativeWindow11.{cpp,h}) +// - https://github.com/google/angle/blob/main/src/libANGLE/renderer/d3d/d3d11/converged/CompositorNativeWindow11.h +// - https://github.com/google/angle/blob/main/src/libANGLE/renderer/d3d/d3d11/converged/CompositorNativeWindow11.cpp +// - https://gist.github.com/clarkezone/43e984fb9bdcd2cfcd9a4f41c208a02f +// +// Copyright 2018 The ANGLE Project Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// +// Neither the name of TransGaming Inc., Google Inc., 3DLabs Inc. +// Ltd., nor the names of their contributors may be used to endorse +// or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +// COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include +#include + +namespace rx { +class RoHelper { + public: + RoHelper(RO_INIT_TYPE init_type); + ~RoHelper(); + bool WinRtAvailable() const; + bool SupportedWindowsRelease(); + HRESULT GetStringReference(PCWSTR source, HSTRING* act, + HSTRING_HEADER* header); + HRESULT GetActivationFactory(const HSTRING act, const IID& interfaceId, + void** fac); + HRESULT WindowsCompareStringOrdinal(HSTRING one, HSTRING two, int* result); + HRESULT CreateDispatcherQueueController( + DispatcherQueueOptions options, + ABI::Windows::System::IDispatcherQueueController** + dispatcherQueueController); + HRESULT WindowsDeleteString(HSTRING one); + HRESULT RoInitialize(RO_INIT_TYPE type); + void RoUninitialize(); + + private: + using WindowsCreateStringReference_ = HRESULT __stdcall(PCWSTR, UINT32, + HSTRING_HEADER*, + HSTRING*); + + using GetActivationFactory_ = HRESULT __stdcall(HSTRING, REFIID, void**); + + using WindowsCompareStringOrginal_ = HRESULT __stdcall(HSTRING, HSTRING, + int*); + + using WindowsDeleteString_ = HRESULT __stdcall(HSTRING); + + using CreateDispatcherQueueController_ = + HRESULT __stdcall(DispatcherQueueOptions, + ABI::Windows::System::IDispatcherQueueController**); + + using RoInitialize_ = HRESULT __stdcall(RO_INIT_TYPE); + using RoUninitialize_ = void __stdcall(); + + WindowsCreateStringReference_* mFpWindowsCreateStringReference; + GetActivationFactory_* mFpGetActivationFactory; + WindowsCompareStringOrginal_* mFpWindowsCompareStringOrdinal; + CreateDispatcherQueueController_* mFpCreateDispatcherQueueController; + WindowsDeleteString_* mFpWindowsDeleteString; + RoInitialize_* mFpRoInitialize; + RoUninitialize_* mFpRoUninitialize; + + bool mWinRtAvailable; + + HMODULE mComBaseModule; + HMODULE mCoreMessagingModule; +}; +} // namespace rx diff --git a/fdGamer/third_party/webview_windows/windows/util/string_converter.cc b/fdGamer/third_party/webview_windows/windows/util/string_converter.cc new file mode 100644 index 00000000..7b2d8861 --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/util/string_converter.cc @@ -0,0 +1,54 @@ +#include "string_converter.h" + +#include + +namespace util { +std::string Utf8FromUtf16(std::wstring_view utf16_string) { + if (utf16_string.empty()) { + return std::string(); + } + + auto src_length = static_cast(utf16_string.size()); + int target_length = + ::WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string.data(), + src_length, nullptr, 0, nullptr, nullptr); + + std::string utf8_string; + if (target_length <= 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string.data(), src_length, + utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} + +std::wstring Utf16FromUtf8(std::string_view utf8_string) { + if (utf8_string.empty()) { + return std::wstring(); + } + + auto src_length = static_cast(utf8_string.size()); + int target_length = + ::MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8_string.data(), + src_length, nullptr, 0); + + std::wstring utf16_string; + if (target_length <= 0 || target_length > utf16_string.max_size()) { + return utf16_string; + } + utf16_string.resize(target_length); + int converted_length = + ::MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8_string.data(), + src_length, utf16_string.data(), target_length); + if (converted_length == 0) { + return std::wstring(); + } + return utf16_string; +} + +} // namespace util diff --git a/fdGamer/third_party/webview_windows/windows/util/string_converter.h b/fdGamer/third_party/webview_windows/windows/util/string_converter.h new file mode 100644 index 00000000..0a71dbe0 --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/util/string_converter.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +namespace util { +std::string Utf8FromUtf16(std::wstring_view utf16_string); +std::wstring Utf16FromUtf8(std::string_view utf8_string); +} // namespace util diff --git a/fdGamer/third_party/webview_windows/windows/util/swizzle.h b/fdGamer/third_party/webview_windows/windows/util/swizzle.h new file mode 100644 index 00000000..67f5f7c6 --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/util/swizzle.h @@ -0,0 +1,192 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +/* + * see skia/src/opts/SkSwizzler_opts.h + */ + +#pragma once + +#include "cpuid/cpuinfo.h" + +/** + * SK_CPU_SSE_LEVEL + * + * If defined, SK_CPU_SSE_LEVEL should be set to the highest supported level. + * On non-intel CPU this should be undefined. + */ +#define SK_CPU_SSE_LEVEL_SSE1 10 +#define SK_CPU_SSE_LEVEL_SSE2 20 +#define SK_CPU_SSE_LEVEL_SSE3 30 +#define SK_CPU_SSE_LEVEL_SSSE3 31 +#define SK_CPU_SSE_LEVEL_SSE41 41 +#define SK_CPU_SSE_LEVEL_SSE42 42 +#define SK_CPU_SSE_LEVEL_AVX 51 +#define SK_CPU_SSE_LEVEL_AVX2 52 +#define SK_CPU_SSE_LEVEL_SKX 60 + +// Are we in GCC/Clang? +#ifndef SK_CPU_SSE_LEVEL +// These checks must be done in descending order to ensure we set the highest +// available SSE level. +#if defined(__AVX512F__) && defined(__AVX512DQ__) && defined(__AVX512CD__) && \ + defined(__AVX512BW__) && defined(__AVX512VL__) +#define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_SKX +#elif defined(__AVX2__) +#define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_AVX2 +#elif defined(__AVX__) +#define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_AVX +#elif defined(__SSE4_2__) +#define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_SSE42 +#elif defined(__SSE4_1__) +#define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_SSE41 +#elif defined(__SSSE3__) +#define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_SSSE3 +#elif defined(__SSE3__) +#define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_SSE3 +#elif defined(__SSE2__) +#define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_SSE2 +#endif +#endif + +// Are we in VisualStudio? +#ifndef SK_CPU_SSE_LEVEL +// These checks must be done in descending order to ensure we set the highest +// available SSE level. 64-bit intel guarantees at least SSE2 support. +#if defined(__AVX512F__) && defined(__AVX512DQ__) && defined(__AVX512CD__) && \ + defined(__AVX512BW__) && defined(__AVX512VL__) +#define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_SKX +#elif defined(__AVX2__) +#define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_AVX2 +#elif defined(__AVX__) +#define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_AVX +#elif defined(_M_X64) || defined(_M_AMD64) +#define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_SSE2 +#elif defined(_M_IX86_FP) +#if _M_IX86_FP >= 2 +#define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_SSE2 +#elif _M_IX86_FP == 1 +#define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_SSE1 +#endif +#endif +#endif + +inline void RGBA_to_BGRA_portable(uint32_t* dst, const uint32_t* src, + int height, int src_stride, int dst_stride) { + auto width = std::min(src_stride, dst_stride); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + uint8_t a = (src[x] >> 24) & 0xFF, b = (src[x] >> 16) & 0xFF, + g = (src[x] >> 8) & 0xFF, r = (src[x] >> 0) & 0xFF; + dst[x] = (uint32_t)a << 24 | (uint32_t)r << 16 | (uint32_t)g << 8 | + (uint32_t)b << 0; + } + + src += src_stride; + dst += dst_stride; + } +} + +#if SK_CPU_SSE_LEVEL >= SK_CPU_SSE_LEVEL_SKX + +inline void RGBA_to_BGRA_SKX(uint32_t* dst, const uint32_t* src, int height, + int src_stride, int dst_stride) { + const uint8_t mask[64] = {2, 1, 0, 3, 6, 5, 4, 7, 10, 9, 8, 11, 14, + 13, 12, 15, 2, 1, 0, 3, 6, 5, 4, 7, 10, 9, + 8, 11, 14, 13, 12, 15, 2, 1, 0, 3, 6, 5, 4, + 7, 10, 9, 8, 11, 14, 13, 12, 15, 2, 1, 0, 3, + 6, 5, 4, 7, 10, 9, 8, 11, 14, 13, 12, 15}; + const __m512i swapRB = _mm512_loadu_si512(mask); + + auto width = std::min(src_stride, dst_stride); + + for (int y = 0; y < height; y++) { + auto cw = width; + auto rptr = src; + auto dptr = dst; + while (cw >= 16) { + __m512i rgba = _mm512_loadu_si512((const __m512i*)rptr); + __m512i bgra = _mm512_shuffle_epi8(rgba, swapRB); + _mm512_storeu_si512((__m512i*)dptr, bgra); + + rptr += 16; + dptr += 16; + cw -= 16; + } + + for (auto x = 0; x < cw; x++) { + uint8_t a = (rptr[x] >> 24) & 0xFF, b = (rptr[x] >> 16) & 0xFF, + g = (rptr[x] >> 8) & 0xFF, r = (rptr[x] >> 0) & 0xFF; + dptr[x] = (uint32_t)a << 24 | (uint32_t)r << 16 | (uint32_t)g << 8 | + (uint32_t)b << 0; + } + + src += src_stride; + dst += dst_stride; + } +} + +#endif + +#if SK_CPU_SSE_LEVEL >= SK_CPU_SSE_LEVEL_AVX2 + +inline void RGBA_to_BGRA_AVX2(uint32_t* dst, const uint32_t* src, int height, + int src_stride, int dst_stride) { + const __m256i swapRB = + _mm256_setr_epi8(2, 1, 0, 3, 6, 5, 4, 7, 10, 9, 8, 11, 14, 13, 12, 15, 2, + 1, 0, 3, 6, 5, 4, 7, 10, 9, 8, 11, 14, 13, 12, 15); + + auto width = std::min(src_stride, dst_stride); + + for (int y = 0; y < height; y++) { + auto cw = width; + auto rptr = src; + auto dptr = dst; + while (cw >= 8) { + __m256i rgba = _mm256_loadu_si256((const __m256i*)rptr); + __m256i bgra = _mm256_shuffle_epi8(rgba, swapRB); + _mm256_storeu_si256((__m256i*)dptr, bgra); + + rptr += 8; + dptr += 8; + cw -= 8; + } + + for (auto x = 0; x < cw; x++) { + uint8_t a = (rptr[x] >> 24) & 0xFF, b = (rptr[x] >> 16) & 0xFF, + g = (rptr[x] >> 8) & 0xFF, r = (rptr[x] >> 0) & 0xFF; + dptr[x] = (uint32_t)a << 24 | (uint32_t)r << 16 | (uint32_t)g << 8 | + (uint32_t)b << 0; + } + + src += src_stride; + dst += dst_stride; + } +} + +#endif + +inline void RGBA_to_BGRA(uint32_t* dst, const uint32_t* src, int height, + int src_stride, int dst_stride) { + static cpuid::cpuinfo info; + +#if SK_CPU_SSE_LEVEL >= SK_CPU_SSE_LEVEL_SKX + if (info.has_avx512_f() && info.has_avx512_dq() && info.has_avx512_cd() && + info.has_avx512_bw() && info.has_avx512_vl()) { + return RGBA_to_BGRA_SKX(dst, src, height, src_stride, dst_stride); + } +#endif + +#if SK_CPU_SSE_LEVEL >= SK_CPU_SSE_LEVEL_AVX2 + if (info.has_avx2()) { + return RGBA_to_BGRA_AVX2(dst, src, height, src_stride, dst_stride); + } +#endif + + RGBA_to_BGRA_portable(dst, src, height, src_stride, dst_stride); +} diff --git a/fdGamer/third_party/webview_windows/windows/webview.cc b/fdGamer/third_party/webview_windows/windows/webview.cc new file mode 100644 index 00000000..5de2fa4e --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/webview.cc @@ -0,0 +1,802 @@ +#include "webview.h" + +#include + +#include +#include + +#include "util/composition.desktop.interop.h" +#include "util/string_converter.h" +#include "webview_host.h" + +using namespace Microsoft::WRL; + +namespace { + +inline void ConvertColor(COREWEBVIEW2_COLOR& webview_color, int32_t color) { + webview_color.B = color & 0xFF; + webview_color.G = (color >> 8) & 0xFF; + webview_color.R = (color >> 16) & 0xFF; + webview_color.A = (color >> 24) & 0xFF; +} + +inline WebviewPermissionKind CW2PermissionKindToPermissionKind( + COREWEBVIEW2_PERMISSION_KIND kind) { + using k = COREWEBVIEW2_PERMISSION_KIND; + switch (kind) { + case k::COREWEBVIEW2_PERMISSION_KIND_MICROPHONE: + return WebviewPermissionKind::Microphone; + case k::COREWEBVIEW2_PERMISSION_KIND_CAMERA: + return WebviewPermissionKind::Camera; + case k::COREWEBVIEW2_PERMISSION_KIND_GEOLOCATION: + return WebviewPermissionKind::GeoLocation; + case k::COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS: + return WebviewPermissionKind::Notifications; + case k::COREWEBVIEW2_PERMISSION_KIND_OTHER_SENSORS: + return WebviewPermissionKind::OtherSensors; + case k::COREWEBVIEW2_PERMISSION_KIND_CLIPBOARD_READ: + return WebviewPermissionKind::ClipboardRead; + default: + return WebviewPermissionKind::Unknown; + } +} + +inline COREWEBVIEW2_PERMISSION_STATE WebViewPermissionStateToCW2PermissionState( + WebviewPermissionState state) { + using s = COREWEBVIEW2_PERMISSION_STATE; + switch (state) { + case WebviewPermissionState::Allow: + return s::COREWEBVIEW2_PERMISSION_STATE_ALLOW; + case WebviewPermissionState::Deny: + return s::COREWEBVIEW2_PERMISSION_STATE_DENY; + default: + return s::COREWEBVIEW2_PERMISSION_STATE_DEFAULT; + } +} + +} // namespace + +Webview::Webview( + wil::com_ptr composition_controller, + WebviewHost* host, HWND hwnd, bool owns_window, bool offscreen_only) + : composition_controller_(std::move(composition_controller)), + host_(host), + hwnd_(hwnd), + owns_window_(owns_window) { + webview_controller_ = + composition_controller_.try_query(); + + if (!webview_controller_ || + FAILED(webview_controller_->get_CoreWebView2(webview_.put()))) { + return; + } + + webview_controller_->put_BoundsMode(COREWEBVIEW2_BOUNDS_MODE_USE_RAW_PIXELS); + webview_controller_->put_ShouldDetectMonitorScaleChanges(FALSE); + webview_controller_->put_RasterizationScale(1.0); + + wil::com_ptr settings; + if (SUCCEEDED(webview_->get_Settings(settings.put()))) { + settings2_ = settings.try_query(); + auto settings3 = settings.try_query(); + + settings->put_IsStatusBarEnabled(FALSE); + settings->put_AreDevToolsEnabled(FALSE); + settings->put_AreDefaultContextMenusEnabled(FALSE); + if (settings3) { + settings3->put_AreBrowserAcceleratorKeysEnabled(FALSE); + } + } + + EnableSecurityUpdates(); + RegisterEventHandlers(); + + is_valid_ = CreateSurface(host->compositor(), hwnd, offscreen_only); +} + +Webview::~Webview() { + if (owns_window_) { + DestroyWindow(hwnd_); + } +} + +bool Webview::CreateSurface( + winrt::com_ptr compositor, + HWND hwnd, bool offscreen_only) { + winrt::com_ptr root; + if (FAILED(compositor->CreateContainerVisual(root.put()))) { + return false; + } + + surface_ = root.try_as(); + assert(surface_); + + // initial size. doesn't matter as we resize the surface anyway. + surface_->put_Size({1280, 720}); + surface_->put_IsVisible(true); + + // Create on-screen window for debugging purposes + if (!offscreen_only) { + window_target_ = util::TryCreateDesktopWindowTarget(compositor, hwnd); + auto composition_target = + window_target_ + .try_as(); + if (composition_target) { + composition_target->put_Root(surface_.get()); + } + } + + winrt::com_ptr webview_visual; + compositor->CreateContainerVisual( + reinterpret_cast( + webview_visual.put())); + + auto webview_visual2 = + webview_visual.try_as(); + if (webview_visual2) { + webview_visual2->put_RelativeSizeAdjustment({1.0f, 1.0f}); + } + + winrt::com_ptr children; + root->get_Children(children.put()); + children->InsertAtTop(webview_visual.get()); + composition_controller_->put_RootVisualTarget(webview_visual2.get()); + + webview_controller_->put_IsVisible(true); + + return true; +} + +void Webview::EnableSecurityUpdates() { + if (SUCCEEDED(webview_->CallDevToolsProtocolMethod(L"Security.enable", L"{}", + nullptr)) && + SUCCEEDED(webview_->GetDevToolsProtocolEventReceiver( + L"Security.securityStateChanged", + &devtools_protocol_event_receiver_))) { + devtools_protocol_event_receiver_->add_DevToolsProtocolEventReceived( + Callback( + [this](ICoreWebView2* sender, + ICoreWebView2DevToolsProtocolEventReceivedEventArgs* args) + -> HRESULT { + if (devtools_protocol_event_callback_) { + wil::unique_cotaskmem_string json_args; + if (args->get_ParameterObjectAsJson(&json_args) == S_OK) { + std::string json = util::Utf8FromUtf16(json_args.get()); + devtools_protocol_event_callback_(json.c_str()); + } + } + + return S_OK; + }) + .Get(), + &event_registrations_.devtools_protocol_event_token_); + } +} + +void Webview::RegisterEventHandlers() { + if (!webview_) { + return; + } + + webview_->add_ContentLoading( + Callback( + [this](ICoreWebView2* sender, IUnknown* args) -> HRESULT { + if (loading_state_changed_callback_) { + loading_state_changed_callback_(WebviewLoadingState::Loading); + } + + return S_OK; + }) + .Get(), + &event_registrations_.content_loading_token_); + + webview_->add_NavigationCompleted( + Callback( + [this](ICoreWebView2* sender, + ICoreWebView2NavigationCompletedEventArgs* args) -> HRESULT { + BOOL is_success; + args->get_IsSuccess(&is_success); + if (!is_success && on_load_error_callback_) { + COREWEBVIEW2_WEB_ERROR_STATUS web_error_status; + args->get_WebErrorStatus(&web_error_status); + on_load_error_callback_(web_error_status); + } + + if (loading_state_changed_callback_) { + loading_state_changed_callback_( + WebviewLoadingState::NavigationCompleted); + } + + return S_OK; + }) + .Get(), + &event_registrations_.navigation_completed_token_); + + webview_->add_HistoryChanged( + Callback( + [this](ICoreWebView2* sender, IUnknown* args) -> HRESULT { + if (history_changed_callback_) { + BOOL can_go_back; + BOOL can_go_forward; + sender->get_CanGoBack(&can_go_back); + sender->get_CanGoForward(&can_go_forward); + history_changed_callback_({can_go_back, can_go_forward}); + } + + return S_OK; + }) + .Get(), + &event_registrations_.history_changed_token_); + + webview_->add_SourceChanged( + Callback( + [this](ICoreWebView2* sender, IUnknown* args) -> HRESULT { + LPWSTR wurl; + if (url_changed_callback_ && webview_->get_Source(&wurl) == S_OK) { + std::string url = util::Utf8FromUtf16(wurl); + url_changed_callback_(url); + } + + return S_OK; + }) + .Get(), + &event_registrations_.source_changed_token_); + + webview_->add_DocumentTitleChanged( + Callback( + [this](ICoreWebView2* sender, IUnknown* args) -> HRESULT { + LPWSTR wtitle; + if (document_title_changed_callback_ && + webview_->get_DocumentTitle(&wtitle) == S_OK) { + std::string title = util::Utf8FromUtf16(wtitle); + document_title_changed_callback_(title); + } + + return S_OK; + }) + .Get(), + &event_registrations_.document_title_changed_token_); + + composition_controller_->add_CursorChanged( + Callback( + [this](ICoreWebView2CompositionController* sender, + IUnknown* args) -> HRESULT { + HCURSOR cursor; + if (cursor_changed_callback_ && + sender->get_Cursor(&cursor) == S_OK) { + cursor_changed_callback_(cursor); + } + return S_OK; + }) + .Get(), + &event_registrations_.cursor_changed_token_); + + webview_controller_->add_GotFocus( + Callback( + [this](ICoreWebView2Controller* sender, IUnknown* args) -> HRESULT { + if (focus_changed_callback_) { + focus_changed_callback_(true); + } + return S_OK; + }) + .Get(), + &event_registrations_.got_focus_token_); + + webview_controller_->add_LostFocus( + Callback( + [this](ICoreWebView2Controller* sender, IUnknown* args) -> HRESULT { + if (focus_changed_callback_) { + focus_changed_callback_(false); + } + return S_OK; + }) + .Get(), + &event_registrations_.lost_focus_token_); + + webview_->add_WebMessageReceived( + Callback( + [this](ICoreWebView2* sender, + ICoreWebView2WebMessageReceivedEventArgs* args) -> HRESULT { + wil::unique_cotaskmem_string wmessage; + if (web_message_received_callback_ && + args->get_WebMessageAsJson(&wmessage) == S_OK) { + const std::string message = util::Utf8FromUtf16(wmessage.get()); + web_message_received_callback_(message); + } + + return S_OK; + }) + .Get(), + &event_registrations_.web_message_received_token_); + + webview_->add_PermissionRequested( + Callback( + [this](ICoreWebView2* sender, + ICoreWebView2PermissionRequestedEventArgs* args) -> HRESULT { + if (!permission_requested_callback_) { + return S_OK; + } + + wil::unique_cotaskmem_string wuri; + COREWEBVIEW2_PERMISSION_KIND kind = + COREWEBVIEW2_PERMISSION_KIND_UNKNOWN_PERMISSION; + BOOL is_user_initiated = false; + + if (args->get_Uri(&wuri) == S_OK && + args->get_PermissionKind(&kind) == S_OK && + args->get_IsUserInitiated(&is_user_initiated) == S_OK) { + wil::com_ptr deferral; + args->GetDeferral(deferral.put()); + + const std::string uri = util::Utf8FromUtf16(wuri.get()); + permission_requested_callback_( + uri, CW2PermissionKindToPermissionKind(kind), + is_user_initiated == TRUE, + [deferral = std::move(deferral), + args = std::move(args)](WebviewPermissionState state) { + args->put_State( + WebViewPermissionStateToCW2PermissionState(state)); + deferral->Complete(); + }); + } + + return S_OK; + }) + .Get(), + &event_registrations_.permission_requested_token_); + + webview_->add_NewWindowRequested( + Callback( + [this](ICoreWebView2* sender, + ICoreWebView2NewWindowRequestedEventArgs* args) -> HRESULT { + switch (popup_window_policy_) { + case WebviewPopupWindowPolicy::Deny: + args->put_Handled(TRUE); + break; + case WebviewPopupWindowPolicy::ShowInSameWindow: + args->put_NewWindow(webview_.get()); + args->put_Handled(TRUE); + break; + } + + return S_OK; + }) + .Get(), + &event_registrations_.new_windows_requested_token_); + + webview_->add_ContainsFullScreenElementChanged( + Callback( + [this](ICoreWebView2* sender, IUnknown* args) -> HRESULT { + BOOL flag = FALSE; + if (contains_fullscreen_element_changed_callback_ && + SUCCEEDED(sender->get_ContainsFullScreenElement(&flag))) { + contains_fullscreen_element_changed_callback_(flag); + } + return S_OK; + }) + .Get(), + &event_registrations_.contains_fullscreen_element_changed_token_); +} + +void Webview::SetSurfaceSize(size_t width, size_t height, float scale_factor) { + if (!IsValid()) { + return; + } + + if (surface_ && width > 0 && height > 0) { + scale_factor_ = scale_factor; + auto scaled_width = width * scale_factor; + auto scaled_height = height * scale_factor; + + RECT bounds; + bounds.left = 0; + bounds.top = 0; + bounds.right = static_cast(scaled_width); + bounds.bottom = static_cast(scaled_height); + + surface_->put_Size({scaled_width, scaled_height}); + webview_controller_->put_RasterizationScale(scale_factor); + if (webview_controller_->put_Bounds(bounds) != S_OK) { + std::cerr << "Setting webview bounds failed." << std::endl; + } + + if (surface_size_changed_callback_) { + surface_size_changed_callback_(width, height); + } + } +} + +bool Webview::OpenDevTools() { + if (!IsValid()) { + return false; + } + webview_->OpenDevToolsWindow(); + return true; +} + +bool Webview::ClearCookies() { + if (!IsValid()) { + return false; + } + return webview_->CallDevToolsProtocolMethod(L"Network.clearBrowserCookies", + L"{}", nullptr) == S_OK; +} + +bool Webview::ClearCache() { + if (!IsValid()) { + return false; + } + return webview_->CallDevToolsProtocolMethod(L"Network.clearBrowserCache", + L"{}", nullptr) == S_OK; +} + +bool Webview::SetCacheDisabled(bool disabled) { + if (!IsValid()) { + return false; + } + std::string json = std::format("{{\"disableCache\":{}}}", disabled); + return webview_->CallDevToolsProtocolMethod(L"Network.setCacheDisabled", + util::Utf16FromUtf8(json).c_str(), + nullptr) == S_OK; +} + +void Webview::SetPopupWindowPolicy(WebviewPopupWindowPolicy policy) { + popup_window_policy_ = policy; +} + +bool Webview::SetUserAgent(const std::string& user_agent) { + if (settings2_) { + return settings2_->put_UserAgent(util::Utf16FromUtf8(user_agent).c_str()) == + S_OK; + } + return false; +} + +bool Webview::SetBackgroundColor(int32_t color) { + if (!IsValid()) { + return false; + } + + COREWEBVIEW2_COLOR webview_color; + ConvertColor(webview_color, color); + + // Semi-transparent backgrounds are not supported. + // Valid alpha values are 0 or 255. + if (webview_color.A > 0) { + webview_color.A = 0xFF; + } + + return webview_controller_->put_DefaultBackgroundColor(webview_color) == S_OK; +} + +bool Webview::SetZoomFactor(double factor) { + if (!IsValid()) { + return false; + } + return webview_controller_->put_ZoomFactor(factor) == S_OK; +} + +void Webview::SetCursorPos(double x, double y) { + if (!IsValid()) { + return; + } + + POINT point; + point.x = static_cast(x * scale_factor_); + point.y = static_cast(y * scale_factor_); + last_cursor_pos_ = point; + + // https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2?view=webview2-1.0.774.44 + composition_controller_->SendMouseInput( + COREWEBVIEW2_MOUSE_EVENT_KIND::COREWEBVIEW2_MOUSE_EVENT_KIND_MOVE, + virtual_keys_.state(), 0, point); +} + +void Webview::SetPointerUpdate(int32_t pointer, + WebviewPointerEventKind eventKind, double x, + double y, double size, double pressure) { + if (!IsValid()) { + return; + } + + COREWEBVIEW2_POINTER_EVENT_KIND event = + COREWEBVIEW2_POINTER_EVENT_KIND_UPDATE; + UINT32 pointerFlags = POINTER_FLAG_NONE; + switch (eventKind) { + case WebviewPointerEventKind::Activate: + event = COREWEBVIEW2_POINTER_EVENT_KIND_ACTIVATE; + break; + case WebviewPointerEventKind::Down: + event = COREWEBVIEW2_POINTER_EVENT_KIND_DOWN; + pointerFlags = + POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT; + break; + case WebviewPointerEventKind::Enter: + event = COREWEBVIEW2_POINTER_EVENT_KIND_ENTER; + break; + case WebviewPointerEventKind::Leave: + event = COREWEBVIEW2_POINTER_EVENT_KIND_LEAVE; + break; + case WebviewPointerEventKind::Up: + event = COREWEBVIEW2_POINTER_EVENT_KIND_UP; + pointerFlags = POINTER_FLAG_UP; + break; + case WebviewPointerEventKind::Update: + event = COREWEBVIEW2_POINTER_EVENT_KIND_UPDATE; + pointerFlags = + POINTER_FLAG_UPDATE | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT; + break; + } + + POINT point; + point.x = static_cast(x * scale_factor_); + point.y = static_cast(y * scale_factor_); + + RECT rect; + rect.left = point.x - 2; + rect.right = point.x + 2; + rect.top = point.y - 2; + rect.bottom = point.y + 2; + + host_->CreateWebViewPointerInfo( + [this, pointer, event, pointerFlags, point, rect, pressure]( + wil::com_ptr pointerInfo, + std::unique_ptr error) { + if (pointerInfo) { + ICoreWebView2PointerInfo* pInfo = pointerInfo.get(); + pInfo->put_PointerId(pointer); + pInfo->put_PointerKind(PT_TOUCH); + pInfo->put_PointerFlags(pointerFlags); + pInfo->put_TouchFlags(TOUCH_FLAG_NONE); + pInfo->put_TouchMask(TOUCH_MASK_CONTACTAREA | TOUCH_MASK_PRESSURE); + pInfo->put_TouchPressure( + std::clamp((UINT32)(pressure == 0.0 ? 1024 : 1024 * pressure), + (UINT32)0, (UINT32)1024)); + pInfo->put_PixelLocationRaw(point); + pInfo->put_TouchContactRaw(rect); + composition_controller_->SendPointerInput(event, pInfo); + } + }); +} + +void Webview::SetPointerButtonState(WebviewPointerButton button, bool is_down) { + if (!IsValid()) { + return; + } + + COREWEBVIEW2_MOUSE_EVENT_KIND kind; + switch (button) { + case WebviewPointerButton::Primary: + virtual_keys_.set_isLeftButtonDown(is_down); + kind = is_down ? COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_DOWN + : COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_UP; + break; + case WebviewPointerButton::Secondary: + virtual_keys_.set_isRightButtonDown(is_down); + kind = is_down ? COREWEBVIEW2_MOUSE_EVENT_KIND_RIGHT_BUTTON_DOWN + : COREWEBVIEW2_MOUSE_EVENT_KIND_RIGHT_BUTTON_UP; + break; + case WebviewPointerButton::Tertiary: + virtual_keys_.set_isMiddleButtonDown(is_down); + kind = is_down ? COREWEBVIEW2_MOUSE_EVENT_KIND_MIDDLE_BUTTON_DOWN + : COREWEBVIEW2_MOUSE_EVENT_KIND_MIDDLE_BUTTON_UP; + break; + default: + kind = static_cast(0); + } + + composition_controller_->SendMouseInput(kind, virtual_keys_.state(), 0, + last_cursor_pos_); +} + +void Webview::SendScroll(double delta, bool horizontal) { + // delta * 6 gives me a multiple of WHEEL_DELTA (120) + constexpr auto kScrollMultiplier = 6; + + auto offset = static_cast(delta * kScrollMultiplier); + + POINT point; + point.x = 0; + point.y = 0; + + if (horizontal) { + composition_controller_->SendMouseInput( + COREWEBVIEW2_MOUSE_EVENT_KIND_HORIZONTAL_WHEEL, virtual_keys_.state(), + offset, point); + } else { + composition_controller_->SendMouseInput(COREWEBVIEW2_MOUSE_EVENT_KIND_WHEEL, + virtual_keys_.state(), offset, + point); + } +} + +void Webview::SetScrollDelta(double delta_x, double delta_y) { + if (!IsValid()) { + return; + } + + if (delta_x != 0.0) { + SendScroll(delta_x, true); + } + if (delta_y != 0.0) { + SendScroll(delta_y, false); + } +} + +void Webview::LoadUrl(const std::string& url) { + if (IsValid()) { + webview_->Navigate(util::Utf16FromUtf8(url).c_str()); + } +} + +void Webview::LoadStringContent(const std::string& content) { + if (IsValid()) { + webview_->NavigateToString(util::Utf16FromUtf8(content).c_str()); + } +} + +bool Webview::Stop() { + if (!IsValid()) { + return false; + } + return SUCCEEDED(webview_->CallDevToolsProtocolMethod(L"Page.stopLoading", + L"{}", nullptr)); +} + +bool Webview::Reload() { + if (!IsValid()) { + return false; + } + return SUCCEEDED(webview_->Reload()); +} + +bool Webview::GoBack() { + if (!IsValid()) { + return false; + } + return SUCCEEDED(webview_->GoBack()); +} + +bool Webview::GoForward() { + if (!IsValid()) { + return false; + } + return SUCCEEDED(webview_->GoForward()); +} + +void Webview::AddScriptToExecuteOnDocumentCreated( + const std::string& script, + AddScriptToExecuteOnDocumentCreatedCallback callback) { + if (IsValid()) { + if (SUCCEEDED(webview_->AddScriptToExecuteOnDocumentCreated( + util::Utf16FromUtf8(script).c_str(), + Callback< + ICoreWebView2AddScriptToExecuteOnDocumentCreatedCompletedHandler>( + [callback](HRESULT result, LPCWSTR wsid) -> HRESULT { + std::string sid = util::Utf8FromUtf16(wsid); + callback(SUCCEEDED(result), sid); + return S_OK; + }) + .Get()))) { + return; + } + } + + callback(false, std::string()); +} + +void Webview::RemoveScriptToExecuteOnDocumentCreated( + const std::string& script_id) { + if (IsValid()) { + webview_->RemoveScriptToExecuteOnDocumentCreated( + util::Utf16FromUtf8(script_id).c_str()); + } +} + +void Webview::ExecuteScript(const std::string& script, + ScriptExecutedCallback callback) { + if (IsValid()) { + if (SUCCEEDED(webview_->ExecuteScript( + util::Utf16FromUtf8(script).c_str(), + Callback( + [callback](HRESULT result, LPCWSTR json_result_object) { + callback(SUCCEEDED(result), + util::Utf8FromUtf16(json_result_object)); + return S_OK; + }) + .Get()))) { + return; + } + } + + callback(false, std::string()); +} + +bool Webview::PostWebMessage(const std::string& json) { + if (!IsValid()) { + return false; + } + return webview_->PostWebMessageAsJson(util::Utf16FromUtf8(json).c_str()) == + S_OK; +} + +bool Webview::Suspend() { + if (!IsValid()) { + return false; + } + + wil::com_ptr webview; + webview = webview_.query(); + if (!webview) { + return false; + } + + webview_controller_->put_IsVisible(false); + return webview->TrySuspend( + Callback( + [](HRESULT error_code, BOOL is_successful) -> HRESULT { + return S_OK; + }) + .Get()) == S_OK; +} + +bool Webview::Resume() { + if (!IsValid()) { + return false; + } + + wil::com_ptr webview; + webview = webview_.query(); + if (!webview) { + return false; + } + return webview->Resume() == S_OK && + webview_controller_->put_IsVisible(true) == S_OK; +} + +bool Webview::SetVirtualHostNameMapping( + const std::string& hostName, const std::string& path, + WebviewHostResourceAccessKind accessKind) { + if (!IsValid()) { + return false; + } + + wil::com_ptr webview; + webview = webview_.query(); + if (!webview) { + return false; + } + + COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND accessKindIntValue = + COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_DENY; + switch (accessKind) { + case WebviewHostResourceAccessKind::Allow: + accessKindIntValue = COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_ALLOW; + break; + case WebviewHostResourceAccessKind::DenyCors: + accessKindIntValue = COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_DENY_CORS; + break; + case WebviewHostResourceAccessKind::Deny: + accessKindIntValue = COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_DENY; + break; + } + + return webview->SetVirtualHostNameToFolderMapping( + util::Utf16FromUtf8(hostName).c_str(), util::Utf16FromUtf8(path).c_str(), + accessKindIntValue); +} + +bool Webview::ClearVirtualHostNameMapping(const std::string& hostName) { + if (!IsValid()) { + return false; + } + + wil::com_ptr webview; + webview = webview_.query(); + if (!webview) { + return false; + } + + return webview->ClearVirtualHostNameToFolderMapping( + util::Utf16FromUtf8(hostName).c_str()); +} diff --git a/fdGamer/third_party/webview_windows/windows/webview.h b/fdGamer/third_party/webview_windows/windows/webview.h new file mode 100644 index 00000000..93033649 --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/webview.h @@ -0,0 +1,259 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +class WebviewHost; + +enum class WebviewLoadingState { None, Loading, NavigationCompleted }; + +enum class WebviewPointerButton { None, Primary, Secondary, Tertiary }; + +enum class WebviewPointerEventKind { Activate, Down, Enter, Leave, Up, Update }; + +enum class WebviewPermissionKind { + Unknown, + Microphone, + Camera, + GeoLocation, + Notifications, + OtherSensors, + ClipboardRead +}; + +enum class WebviewPermissionState { Default, Allow, Deny }; + +enum class WebviewPopupWindowPolicy { Allow, Deny, ShowInSameWindow }; + +enum class WebviewHostResourceAccessKind { Deny, Allow, DenyCors }; + +struct WebviewHistoryChanged { + BOOL can_go_back; + BOOL can_go_forward; +}; + +struct VirtualKeyState { + public: + inline void set_isLeftButtonDown(bool is_down) { + set(COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS:: + COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_LEFT_BUTTON, + is_down); + } + + inline void set_isRightButtonDown(bool is_down) { + set(COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS:: + COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_RIGHT_BUTTON, + is_down); + } + + inline void set_isMiddleButtonDown(bool is_down) { + set(COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS:: + COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_MIDDLE_BUTTON, + is_down); + } + + inline COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS state() const { return state_; } + + private: + COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS state_ = + COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS:: + COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_NONE; + + inline void set(COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS key, bool flag) { + if (flag) { + state_ |= key; + } else { + state_ &= ~key; + } + } +}; + +struct EventRegistrations { + EventRegistrationToken source_changed_token_{}; + EventRegistrationToken content_loading_token_{}; + EventRegistrationToken navigation_completed_token_{}; + EventRegistrationToken history_changed_token_{}; + EventRegistrationToken document_title_changed_token_{}; + EventRegistrationToken cursor_changed_token_{}; + EventRegistrationToken got_focus_token_{}; + EventRegistrationToken lost_focus_token_{}; + EventRegistrationToken web_message_received_token_{}; + EventRegistrationToken permission_requested_token_{}; + EventRegistrationToken devtools_protocol_event_token_{}; + EventRegistrationToken new_windows_requested_token_{}; + EventRegistrationToken contains_fullscreen_element_changed_token_{}; +}; + +class Webview { + public: + friend class WebviewHost; + + typedef std::function UrlChangedCallback; + typedef std::function LoadingStateChangedCallback; + typedef std::function + OnLoadErrorCallback; + typedef std::function HistoryChangedCallback; + typedef std::function DevtoolsProtocolEventCallback; + typedef std::function DocumentTitleChangedCallback; + typedef std::function + SurfaceSizeChangedCallback; + typedef std::function CursorChangedCallback; + typedef std::function FocusChangedCallback; + typedef std::function + AddScriptToExecuteOnDocumentCreatedCallback; + typedef std::function ScriptExecutedCallback; + typedef std::function WebMessageReceivedCallback; + typedef std::function + WebviewPermissionRequestedCompleter; + typedef std::function + PermissionRequestedCallback; + typedef std::function + ContainsFullScreenElementChangedCallback; + + ~Webview(); + + ABI::Windows::UI::Composition::IVisual* const surface() { + return surface_.get(); + } + + bool IsValid() { return is_valid_; } + + void SetSurfaceSize(size_t width, size_t height, float scale_factor); + void SetCursorPos(double x, double y); + void SetPointerUpdate(int32_t pointer, WebviewPointerEventKind eventKind, + double x, double y, double size, double pressure); + void SetPointerButtonState(WebviewPointerButton button, bool isDown); + void SetScrollDelta(double delta_x, double delta_y); + void LoadUrl(const std::string& url); + void LoadStringContent(const std::string& content); + bool Stop(); + bool Reload(); + bool GoBack(); + bool GoForward(); + void AddScriptToExecuteOnDocumentCreated( + const std::string& script, + AddScriptToExecuteOnDocumentCreatedCallback callback); + void RemoveScriptToExecuteOnDocumentCreated(const std::string& script_id); + void ExecuteScript(const std::string& script, + ScriptExecutedCallback callback); + bool PostWebMessage(const std::string& json); + bool ClearCookies(); + bool ClearCache(); + bool SetCacheDisabled(bool disabled); + void SetPopupWindowPolicy(WebviewPopupWindowPolicy policy); + bool SetUserAgent(const std::string& user_agent); + bool OpenDevTools(); + bool SetBackgroundColor(int32_t color); + bool SetZoomFactor(double factor); + bool Suspend(); + bool Resume(); + + bool SetVirtualHostNameMapping(const std::string& hostName, + const std::string& path, + WebviewHostResourceAccessKind accessKind); + bool ClearVirtualHostNameMapping(const std::string& hostName); + + void OnUrlChanged(UrlChangedCallback callback) { + url_changed_callback_ = std::move(callback); + } + + void OnLoadError(OnLoadErrorCallback callback) { + on_load_error_callback_ = std::move(callback); + } + + void OnLoadingStateChanged(LoadingStateChangedCallback callback) { + loading_state_changed_callback_ = std::move(callback); + } + + void OnHistoryChanged(HistoryChangedCallback callback) { + history_changed_callback_ = std::move(callback); + } + + void OnSurfaceSizeChanged(SurfaceSizeChangedCallback callback) { + surface_size_changed_callback_ = std::move(callback); + } + + void OnDocumentTitleChanged(DocumentTitleChangedCallback callback) { + document_title_changed_callback_ = std::move(callback); + } + + void OnCursorChanged(CursorChangedCallback callback) { + cursor_changed_callback_ = std::move(callback); + } + + void OnFocusChanged(FocusChangedCallback callback) { + focus_changed_callback_ = std::move(callback); + } + + void OnWebMessageReceived(WebMessageReceivedCallback callback) { + web_message_received_callback_ = std::move(callback); + } + + void OnPermissionRequested(PermissionRequestedCallback callback) { + permission_requested_callback_ = std::move(callback); + } + + void OnDevtoolsProtocolEvent(DevtoolsProtocolEventCallback callback) { + devtools_protocol_event_callback_ = std::move(callback); + } + + void OnContainsFullScreenElementChanged( + ContainsFullScreenElementChangedCallback callback) { + contains_fullscreen_element_changed_callback_ = std::move(callback); + } + + private: + HWND hwnd_; + bool owns_window_; + bool is_valid_ = false; + float scale_factor_ = 1.0; + wil::com_ptr composition_controller_; + wil::com_ptr webview_controller_; + wil::com_ptr webview_; + wil::com_ptr + devtools_protocol_event_receiver_; + wil::com_ptr settings2_; + POINT last_cursor_pos_ = {0, 0}; + VirtualKeyState virtual_keys_; + WebviewPopupWindowPolicy popup_window_policy_ = + WebviewPopupWindowPolicy::Allow; + + winrt::com_ptr surface_; + winrt::com_ptr + window_target_; + + WebviewHost* host_; + EventRegistrations event_registrations_{}; + + UrlChangedCallback url_changed_callback_; + LoadingStateChangedCallback loading_state_changed_callback_; + OnLoadErrorCallback on_load_error_callback_; + HistoryChangedCallback history_changed_callback_; + DocumentTitleChangedCallback document_title_changed_callback_; + SurfaceSizeChangedCallback surface_size_changed_callback_; + CursorChangedCallback cursor_changed_callback_; + FocusChangedCallback focus_changed_callback_; + WebMessageReceivedCallback web_message_received_callback_; + PermissionRequestedCallback permission_requested_callback_; + DevtoolsProtocolEventCallback devtools_protocol_event_callback_; + ContainsFullScreenElementChangedCallback + contains_fullscreen_element_changed_callback_; + + Webview( + wil::com_ptr composition_controller, + WebviewHost* host, HWND hwnd, bool owns_window, bool offscreen_only); + + bool CreateSurface( + winrt::com_ptr compositor, + HWND hwnd, bool offscreen_only); + void RegisterEventHandlers(); + void EnableSecurityUpdates(); + void SendScroll(double offset, bool horizontal); +}; diff --git a/fdGamer/third_party/webview_windows/windows/webview_bridge.cc b/fdGamer/third_party/webview_windows/windows/webview_bridge.cc new file mode 100644 index 00000000..1a5d1ced --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/webview_bridge.cc @@ -0,0 +1,692 @@ +#include "webview_bridge.h" + +#include +#include + +#include + +#ifdef HAVE_FLUTTER_D3D_TEXTURE +#include "texture_bridge_gpu.h" +#else +#include "texture_bridge_fallback.h" +#endif + +namespace { +constexpr auto kErrorInvalidArgs = "invalidArguments"; + +constexpr auto kMethodLoadUrl = "loadUrl"; +constexpr auto kMethodLoadStringContent = "loadStringContent"; +constexpr auto kMethodReload = "reload"; +constexpr auto kMethodStop = "stop"; +constexpr auto kMethodGoBack = "goBack"; +constexpr auto kMethodGoForward = "goForward"; +constexpr auto kMethodAddScriptToExecuteOnDocumentCreated = + "addScriptToExecuteOnDocumentCreated"; +constexpr auto kMethodRemoveScriptToExecuteOnDocumentCreated = + "removeScriptToExecuteOnDocumentCreated"; +constexpr auto kMethodExecuteScript = "executeScript"; +constexpr auto kMethodPostWebMessage = "postWebMessage"; +constexpr auto kMethodSetSize = "setSize"; +constexpr auto kMethodSetCursorPos = "setCursorPos"; +constexpr auto kMethodSetPointerUpdate = "setPointerUpdate"; +constexpr auto kMethodSetPointerButton = "setPointerButton"; +constexpr auto kMethodSetScrollDelta = "setScrollDelta"; +constexpr auto kMethodSetUserAgent = "setUserAgent"; +constexpr auto kMethodSetBackgroundColor = "setBackgroundColor"; +constexpr auto kMethodSetZoomFactor = "setZoomFactor"; +constexpr auto kMethodOpenDevTools = "openDevTools"; +constexpr auto kMethodSuspend = "suspend"; +constexpr auto kMethodResume = "resume"; +constexpr auto kMethodSetVirtualHostNameMapping = "setVirtualHostNameMapping"; +constexpr auto kMethodClearVirtualHostNameMapping = + "clearVirtualHostNameMapping"; +constexpr auto kMethodClearCookies = "clearCookies"; +constexpr auto kMethodClearCache = "clearCache"; +constexpr auto kMethodSetCacheDisabled = "setCacheDisabled"; +constexpr auto kMethodSetPopupWindowPolicy = "setPopupWindowPolicy"; +constexpr auto kMethodSetFpsLimit = "setFpsLimit"; + +constexpr auto kEventType = "type"; +constexpr auto kEventValue = "value"; + +constexpr auto kErrorNotSupported = "not_supported"; +constexpr auto kScriptFailed = "script_failed"; +constexpr auto kMethodFailed = "method_failed"; + +static const std::optional> GetPointFromArgs( + const flutter::EncodableValue* args) { + const flutter::EncodableList* list = + std::get_if(args); + if (!list || list->size() != 2) { + return std::nullopt; + } + const auto x = std::get_if(&(*list)[0]); + const auto y = std::get_if(&(*list)[1]); + if (!x || !y) { + return std::nullopt; + } + return std::make_pair(*x, *y); +} + +static const std::optional> +GetPointAndScaleFactorFromArgs(const flutter::EncodableValue* args) { + const flutter::EncodableList* list = + std::get_if(args); + if (!list || list->size() != 3) { + return std::nullopt; + } + const auto x = std::get_if(&(*list)[0]); + const auto y = std::get_if(&(*list)[1]); + const auto z = std::get_if(&(*list)[2]); + if (!x || !y || !z) { + return std::nullopt; + } + return std::make_tuple(*x, *y, *z); +} + +static const std::string& GetCursorName(const HCURSOR cursor) { + // The cursor names correspond to the Flutter Engine names: + // in shell/platform/windows/flutter_window_win32.cc + static const std::string kDefaultCursorName = "basic"; + static const std::pair mappings[] = { + {"allScroll", IDC_SIZEALL}, + {kDefaultCursorName, IDC_ARROW}, + {"click", IDC_HAND}, + {"forbidden", IDC_NO}, + {"help", IDC_HELP}, + {"move", IDC_SIZEALL}, + {"none", nullptr}, + {"noDrop", IDC_NO}, + {"precise", IDC_CROSS}, + {"progress", IDC_APPSTARTING}, + {"text", IDC_IBEAM}, + {"resizeColumn", IDC_SIZEWE}, + {"resizeDown", IDC_SIZENS}, + {"resizeDownLeft", IDC_SIZENESW}, + {"resizeDownRight", IDC_SIZENWSE}, + {"resizeLeft", IDC_SIZEWE}, + {"resizeLeftRight", IDC_SIZEWE}, + {"resizeRight", IDC_SIZEWE}, + {"resizeRow", IDC_SIZENS}, + {"resizeUp", IDC_SIZENS}, + {"resizeUpDown", IDC_SIZENS}, + {"resizeUpLeft", IDC_SIZENWSE}, + {"resizeUpRight", IDC_SIZENESW}, + {"resizeUpLeftDownRight", IDC_SIZENWSE}, + {"resizeUpRightDownLeft", IDC_SIZENESW}, + {"wait", IDC_WAIT}, + }; + + static std::map cursors; + static bool initialized = false; + + if (!initialized) { + initialized = true; + for (const auto& pair : mappings) { + HCURSOR cursor_handle = LoadCursor(nullptr, pair.second); + if (cursor_handle) { + cursors[cursor_handle] = pair.first; + } + } + } + + const auto it = cursors.find(cursor); + if (it != cursors.end()) { + return it->second; + } + return kDefaultCursorName; +} + +} // namespace + +WebviewBridge::WebviewBridge(flutter::BinaryMessenger* messenger, + flutter::TextureRegistrar* texture_registrar, + GraphicsContext* graphics_context, + std::unique_ptr webview) + : webview_(std::move(webview)), texture_registrar_(texture_registrar) { +#ifdef HAVE_FLUTTER_D3D_TEXTURE + texture_bridge_ = + std::make_unique(graphics_context, webview_->surface()); + + flutter_texture_ = + std::make_unique(flutter::GpuSurfaceTexture( + kFlutterDesktopGpuSurfaceTypeDxgiSharedHandle, + [bridge = static_cast(texture_bridge_.get())]( + size_t width, + size_t height) -> const FlutterDesktopGpuSurfaceDescriptor* { + return bridge->GetSurfaceDescriptor(width, height); + })); +#else + texture_bridge_ = std::make_unique( + graphics_context, webview_->surface()); + + flutter_texture_ = + std::make_unique(flutter::PixelBufferTexture( + [bridge = static_cast(texture_bridge_.get())]( + size_t width, size_t height) -> const FlutterDesktopPixelBuffer* { + return bridge->CopyPixelBuffer(width, height); + })); +#endif + + texture_id_ = texture_registrar->RegisterTexture(flutter_texture_.get()); + texture_bridge_->SetOnFrameAvailable( + [this]() { texture_registrar_->MarkTextureFrameAvailable(texture_id_); }); + // texture_bridge_->SetOnSurfaceSizeChanged([this](Size size) { + // webview_->SetSurfaceSize(size.width, size.height); + //}); + + const auto method_channel_name = + std::format("io.jns.webview.win/{}", texture_id_); + method_channel_ = + std::make_unique>( + messenger, method_channel_name, + &flutter::StandardMethodCodec::GetInstance()); + method_channel_->SetMethodCallHandler([this](const auto& call, auto result) { + HandleMethodCall(call, std::move(result)); + }); + + const auto event_channel_name = + std::format("io.jns.webview.win/{}/events", texture_id_); + event_channel_ = + std::make_unique>( + messenger, event_channel_name, + &flutter::StandardMethodCodec::GetInstance()); + + auto handler = std::make_unique< + flutter::StreamHandlerFunctions>( + [this](const flutter::EncodableValue* arguments, + std::unique_ptr>&& + events) { + event_sink_ = std::move(events); + RegisterEventHandlers(); + return nullptr; + }, + [this](const flutter::EncodableValue* arguments) { + event_sink_ = nullptr; + return nullptr; + }); + + event_channel_->SetStreamHandler(std::move(handler)); +} + +WebviewBridge::~WebviewBridge() { + method_channel_->SetMethodCallHandler(nullptr); + texture_registrar_->UnregisterTexture(texture_id_); +} + +void WebviewBridge::RegisterEventHandlers() { + webview_->OnUrlChanged([this](const std::string& url) { + const auto event = flutter::EncodableValue(flutter::EncodableMap{ + {flutter::EncodableValue(kEventType), + flutter::EncodableValue("urlChanged")}, + {flutter::EncodableValue(kEventValue), flutter::EncodableValue(url)}, + }); + EmitEvent(event); + }); + + webview_->OnLoadError([this](COREWEBVIEW2_WEB_ERROR_STATUS web_status) { + const auto event = flutter::EncodableValue(flutter::EncodableMap{ + {flutter::EncodableValue(kEventType), + flutter::EncodableValue("onLoadError")}, + {flutter::EncodableValue(kEventValue), + flutter::EncodableValue(static_cast(web_status))}, + }); + EmitEvent(event); + }); + + webview_->OnLoadingStateChanged([this](WebviewLoadingState state) { + const auto event = flutter::EncodableValue(flutter::EncodableMap{ + {flutter::EncodableValue(kEventType), + flutter::EncodableValue("loadingStateChanged")}, + {flutter::EncodableValue(kEventValue), + flutter::EncodableValue(static_cast(state))}, + }); + EmitEvent(event); + }); + + webview_->OnHistoryChanged([this](WebviewHistoryChanged historyChanged) { + const auto event = flutter::EncodableValue(flutter::EncodableMap{ + {flutter::EncodableValue(kEventType), + flutter::EncodableValue("historyChanged")}, + {flutter::EncodableValue(kEventValue), + flutter::EncodableValue(flutter::EncodableMap{ + {flutter::EncodableValue("canGoBack"), + flutter::EncodableValue( + static_cast(historyChanged.can_go_back))}, + {flutter::EncodableValue("canGoForward"), + flutter::EncodableValue( + static_cast(historyChanged.can_go_forward))}, + })}, + }); + EmitEvent(event); + }); + + webview_->OnDevtoolsProtocolEvent([this](const std::string& json) { + const auto event = flutter::EncodableValue(flutter::EncodableMap{ + {flutter::EncodableValue(kEventType), + flutter::EncodableValue("securityStateChanged")}, + {flutter::EncodableValue(kEventValue), flutter::EncodableValue(json)}}); + EmitEvent(event); + }); + + webview_->OnDocumentTitleChanged([this](const std::string& title) { + const auto event = flutter::EncodableValue(flutter::EncodableMap{ + {flutter::EncodableValue(kEventType), + flutter::EncodableValue("titleChanged")}, + {flutter::EncodableValue(kEventValue), flutter::EncodableValue(title)}, + }); + EmitEvent(event); + }); + + webview_->OnSurfaceSizeChanged([this](size_t width, size_t height) { + texture_bridge_->NotifySurfaceSizeChanged(); + }); + + webview_->OnCursorChanged([this](const HCURSOR cursor) { + const auto& name = GetCursorName(cursor); + const auto event = flutter::EncodableValue( + flutter::EncodableMap{{flutter::EncodableValue(kEventType), + flutter::EncodableValue("cursorChanged")}, + {flutter::EncodableValue(kEventValue), name}}); + EmitEvent(event); + }); + + webview_->OnWebMessageReceived([this](const std::string& message) { + const auto event = flutter::EncodableValue( + flutter::EncodableMap{{flutter::EncodableValue(kEventType), + flutter::EncodableValue("webMessageReceived")}, + {flutter::EncodableValue(kEventValue), message}}); + EmitEvent(event); + }); + + webview_->OnPermissionRequested( + [this](const std::string& url, WebviewPermissionKind kind, + bool is_user_initiated, + Webview::WebviewPermissionRequestedCompleter completer) { + OnPermissionRequested(url, kind, is_user_initiated, completer); + }); + + webview_->OnContainsFullScreenElementChanged( + [this](bool contains_fullscreen_element) { + const auto event = flutter::EncodableValue(flutter::EncodableMap{ + {flutter::EncodableValue(kEventType), + flutter::EncodableValue("containsFullScreenElementChanged")}, + {flutter::EncodableValue(kEventValue), + contains_fullscreen_element}}); + EmitEvent(event); + }); +} + +void WebviewBridge::OnPermissionRequested( + const std::string& url, WebviewPermissionKind permissionKind, + bool isUserInitiated, + Webview::WebviewPermissionRequestedCompleter completer) { + auto args = std::make_unique(flutter::EncodableMap{ + {"url", url}, + {"isUserInitiated", isUserInitiated}, + {"permissionKind", static_cast(permissionKind)}}); + + method_channel_->InvokeMethod( + "permissionRequested", std::move(args), + std::make_unique>( + [completer](const flutter::EncodableValue* result) { + auto allow = std::get_if(result); + if (allow != nullptr) { + return completer(*allow ? WebviewPermissionState::Allow + : WebviewPermissionState::Deny); + } + completer(WebviewPermissionState::Default); + }, + [completer](const std::string& error_code, + const std::string& error_message, + const flutter::EncodableValue* error_details) { + completer(WebviewPermissionState::Default); + }, + [completer]() { completer(WebviewPermissionState::Default); })); +} + +void WebviewBridge::HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result) { + const auto& method_name = method_call.method_name(); + + // setCursorPos: [double x, double y] + if (method_name.compare(kMethodSetCursorPos) == 0) { + const auto point = GetPointFromArgs(method_call.arguments()); + if (point) { + webview_->SetCursorPos(point->first, point->second); + return result->Success(); + } + return result->Error(kErrorInvalidArgs); + } + + // setPointerUpdate: + // [int pointer, int event, double x, double y, double size, double pressure] + if (method_name.compare(kMethodSetPointerUpdate) == 0) { + const flutter::EncodableList* list = + std::get_if(method_call.arguments()); + if (!list || list->size() != 6) { + return result->Error(kErrorInvalidArgs); + } + + const auto pointer = std::get_if(&(*list)[0]); + const auto event = std::get_if(&(*list)[1]); + const auto x = std::get_if(&(*list)[2]); + const auto y = std::get_if(&(*list)[3]); + const auto size = std::get_if(&(*list)[4]); + const auto pressure = std::get_if(&(*list)[5]); + + if (pointer && event && x && y && size && pressure) { + webview_->SetPointerUpdate(*pointer, + static_cast(*event), + *x, *y, *size, *pressure); + return result->Success(); + } + return result->Error(kErrorInvalidArgs); + } + + // setScrollDelta: [double dx, double dy] + if (method_name.compare(kMethodSetScrollDelta) == 0) { + const auto delta = GetPointFromArgs(method_call.arguments()); + if (delta) { + webview_->SetScrollDelta(delta->first, delta->second); + return result->Success(); + } + return result->Error(kErrorInvalidArgs); + } + + // setPointerButton: {"button": int, "isDown": bool} + if (method_name.compare(kMethodSetPointerButton) == 0) { + const auto& map = std::get(*method_call.arguments()); + + const auto button = map.find(flutter::EncodableValue("button")); + const auto isDown = map.find(flutter::EncodableValue("isDown")); + if (button != map.end() && isDown != map.end()) { + const auto buttonValue = std::get_if(&button->second); + const auto isDownValue = std::get_if(&isDown->second); + if (buttonValue && isDownValue) { + webview_->SetPointerButtonState( + static_cast(*buttonValue), *isDownValue); + return result->Success(); + } + } + return result->Error(kErrorInvalidArgs); + } + + // setSize: [double width, double height, double scale_factor] + if (method_name.compare(kMethodSetSize) == 0) { + auto size = GetPointAndScaleFactorFromArgs(method_call.arguments()); + if (size) { + const auto [width, height, scale_factor] = size.value(); + + webview_->SetSurfaceSize(static_cast(width), + static_cast(height), + static_cast(scale_factor)); + + texture_bridge_->Start(); + return result->Success(); + } + return result->Error(kErrorInvalidArgs); + } + + // loadUrl: string + if (method_name.compare(kMethodLoadUrl) == 0) { + if (const auto url = std::get_if(method_call.arguments())) { + webview_->LoadUrl(*url); + return result->Success(); + } + return result->Error(kErrorInvalidArgs); + } + + // loadStringContent: string + if (method_name.compare(kMethodLoadStringContent) == 0) { + if (const auto content = + std::get_if(method_call.arguments())) { + webview_->LoadStringContent(*content); + return result->Success(); + } + return result->Error(kErrorInvalidArgs); + } + + // reload + if (method_name.compare(kMethodReload) == 0) { + if (webview_->Reload()) { + return result->Success(); + } + return result->Error(kMethodFailed); + } + + // stop + if (method_name.compare(kMethodStop) == 0) { + if (webview_->Stop()) { + return result->Success(); + } + return result->Error(kMethodFailed); + } + + // goBack + if (method_name.compare(kMethodGoBack) == 0) { + if (webview_->GoBack()) { + return result->Success(); + } + return result->Error(kMethodFailed); + } + + // goForward + if (method_name.compare(kMethodGoForward) == 0) { + if (webview_->GoForward()) { + return result->Success(); + } + return result->Error(kMethodFailed); + } + + // suspend + if (method_name.compare(kMethodSuspend) == 0) { + texture_bridge_->Stop(); + webview_->Suspend(); + return result->Success(); + } + + // resume + if (method_name.compare(kMethodResume) == 0) { + webview_->Resume(); + texture_bridge_->Start(); + return result->Success(); + } + + // setVirtualHostNameMapping [string hostName, string path, int accessKind] + if (method_name.compare(kMethodSetVirtualHostNameMapping) == 0) { + const flutter::EncodableList* list = + std::get_if(method_call.arguments()); + if (!list || list->size() != 3) { + return result->Error(kErrorInvalidArgs); + } + + const auto hostName = std::get_if(&(*list)[0]); + const auto path = std::get_if(&(*list)[1]); + const auto accessKind = std::get_if(&(*list)[2]); + + if (hostName && path && accessKind) { + webview_->SetVirtualHostNameMapping( + *hostName, *path, + static_cast(*accessKind)); + return result->Success(); + } + return result->Error(kErrorInvalidArgs); + } + + // clearVirtualHostNameMapping: string + if (method_name.compare(kMethodClearVirtualHostNameMapping) == 0) { + if (const auto hostName = + std::get_if(method_call.arguments())) { + if (webview_->ClearVirtualHostNameMapping(*hostName)) { + return result->Success(); + } + } + return result->Error(kErrorInvalidArgs); + } + + if (method_name.compare(kMethodAddScriptToExecuteOnDocumentCreated) == 0) { + if (const auto script = std::get_if(method_call.arguments())) { + std::shared_ptr> + shared_result = std::move(result); + + webview_->AddScriptToExecuteOnDocumentCreated( + *script, [shared_result](bool success, const std::string& script_id) { + if (success) { + shared_result->Success(script_id); + } else { + shared_result->Error(kScriptFailed, "Executing script failed."); + } + }); + return; + } + return result->Error(kErrorInvalidArgs); + } + + if (method_name.compare(kMethodRemoveScriptToExecuteOnDocumentCreated) == 0) { + if (const auto script_id = + std::get_if(method_call.arguments())) { + std::shared_ptr> + shared_result = std::move(result); + + webview_->RemoveScriptToExecuteOnDocumentCreated(*script_id); + shared_result->Success(); + return; + } + return result->Error(kErrorInvalidArgs); + } + + // executeScript: string + if (method_name.compare(kMethodExecuteScript) == 0) { + if (const auto script = std::get_if(method_call.arguments())) { + std::shared_ptr> + shared_result = std::move(result); + + webview_->ExecuteScript( + *script, + [shared_result](bool success, const std::string& json_result) { + if (success) { + shared_result->Success(json_result); + } else { + shared_result->Error(kScriptFailed, "Executing script failed."); + } + }); + return; + } + return result->Error(kErrorInvalidArgs); + } + + // postWebMessage: string + if (method_name.compare(kMethodPostWebMessage) == 0) { + if (const auto message = + std::get_if(method_call.arguments())) { + if (webview_->PostWebMessage(*message)) { + return result->Success(); + } + return result->Error(kErrorNotSupported, "Posting the message failed."); + } + return result->Error(kErrorInvalidArgs); + } + + // setUserAgent: string + if (method_name.compare(kMethodSetUserAgent) == 0) { + if (const auto user_agent = + std::get_if(method_call.arguments())) { + if (webview_->SetUserAgent(*user_agent)) { + return result->Success(); + } + return result->Error(kErrorNotSupported, + "Setting the user agent failed."); + } + return result->Error(kErrorInvalidArgs); + } + + // setBackgroundColor: int + if (method_name.compare(kMethodSetBackgroundColor) == 0) { + if (const auto color = std::get_if(method_call.arguments())) { + if (webview_->SetBackgroundColor(*color)) { + return result->Success(); + } + return result->Error(kErrorNotSupported, + "Setting the background color failed."); + } + return result->Error(kErrorInvalidArgs); + } + + // setZoomFactor: double + if (method_name.compare(kMethodSetZoomFactor) == 0) { + if (const auto factor = std::get_if(method_call.arguments())) { + if (webview_->SetZoomFactor(*factor)) { + return result->Success(); + } + return result->Error(kErrorNotSupported, + "Setting the zoom factor failed."); + } + return result->Error(kErrorInvalidArgs); + } + + // openDevTools + if (method_name.compare(kMethodOpenDevTools) == 0) { + if (webview_->OpenDevTools()) { + return result->Success(); + } + return result->Error(kMethodFailed); + } + + // clearCookies + if (method_name.compare(kMethodClearCookies) == 0) { + if (webview_->ClearCookies()) { + return result->Success(); + } + return result->Error(kMethodFailed); + } + + // clearCache + if (method_name.compare(kMethodClearCache) == 0) { + if (webview_->ClearCache()) { + return result->Success(); + } + return result->Error(kMethodFailed); + } + + // setCacheDisabled: bool + if (method_name.compare(kMethodSetCacheDisabled) == 0) { + if (const auto disabled = std::get_if(method_call.arguments())) { + if (webview_->SetCacheDisabled(*disabled)) { + return result->Success(); + } + } + return result->Error(kErrorInvalidArgs); + } + + // setPopupWindowPolicy: int + if (method_name.compare(kMethodSetPopupWindowPolicy) == 0) { + if (const auto index = std::get_if(method_call.arguments())) { + switch (*index) { + case 1: + webview_->SetPopupWindowPolicy(WebviewPopupWindowPolicy::Deny); + break; + case 2: + webview_->SetPopupWindowPolicy( + WebviewPopupWindowPolicy::ShowInSameWindow); + break; + default: + webview_->SetPopupWindowPolicy(WebviewPopupWindowPolicy::Allow); + break; + } + return result->Success(); + } + return result->Error(kErrorInvalidArgs); + } + + if (method_name.compare(kMethodSetFpsLimit) == 0) { + if (const auto value = std::get_if(method_call.arguments())) { + texture_bridge_->SetFpsLimit(*value == 0 ? std::nullopt + : std::make_optional(*value)); + return result->Success(); + } + } + + result->NotImplemented(); +} diff --git a/fdGamer/third_party/webview_windows/windows/webview_bridge.h b/fdGamer/third_party/webview_windows/windows/webview_bridge.h new file mode 100644 index 00000000..18723296 --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/webview_bridge.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include +#include + +#include + +#include "graphics_context.h" +#include "texture_bridge.h" +#include "webview.h" + +class WebviewBridge { + public: + WebviewBridge(flutter::BinaryMessenger* messenger, + flutter::TextureRegistrar* texture_registrar, + GraphicsContext* graphics_context, + std::unique_ptr webview); + ~WebviewBridge(); + + TextureBridge* texture_bridge() const { return texture_bridge_.get(); } + + int64_t texture_id() const { return texture_id_; } + + private: + std::unique_ptr flutter_texture_; + std::unique_ptr texture_bridge_; + std::unique_ptr webview_; + std::unique_ptr> event_sink_; + std::unique_ptr> + event_channel_; + std::unique_ptr> + method_channel_; + + flutter::TextureRegistrar* texture_registrar_; + int64_t texture_id_; + + void HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result); + void RegisterEventHandlers(); + + template + void EmitEvent(const T& value) { + if (event_sink_) { + event_sink_->Success(value); + } + } + + void OnPermissionRequested( + const std::string& url, WebviewPermissionKind permissionKind, + bool is_user_initiated, + Webview::WebviewPermissionRequestedCompleter completer); +}; diff --git a/fdGamer/third_party/webview_windows/windows/webview_host.cc b/fdGamer/third_party/webview_windows/windows/webview_host.cc new file mode 100644 index 00000000..275a8529 --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/webview_host.cc @@ -0,0 +1,117 @@ +#include "webview_host.h" + +#include + +#include +#include + +#include "util/rohelper.h" + +using namespace Microsoft::WRL; + +// static +std::unique_ptr WebviewHost::Create( + WebviewPlatform* platform, std::optional user_data_directory, + std::optional browser_exe_path, + std::optional arguments) { + wil::com_ptr opts; + if (arguments.has_value()) { + opts = Microsoft::WRL::Make(); + std::wstring warguments(arguments.value().begin(), arguments.value().end()); + opts->put_AdditionalBrowserArguments(warguments.c_str()); + } + + std::promise result_promise; + wil::com_ptr env; + auto result = CreateCoreWebView2EnvironmentWithOptions( + browser_exe_path.has_value() ? browser_exe_path->c_str() : nullptr, + user_data_directory.has_value() ? user_data_directory->c_str() : nullptr, opts.get(), + Callback( + [&promise = result_promise, &ptr = env]( + HRESULT r, ICoreWebView2Environment* env) -> HRESULT { + promise.set_value(r); + ptr.swap(env); + return S_OK; + }) + .Get()); + + if (SUCCEEDED(result)) { + result = result_promise.get_future().get(); + if ((SUCCEEDED(result) || result == RPC_E_CHANGED_MODE) && env) { + auto webview_env3 = env.try_query(); + if (webview_env3) { + return std::unique_ptr( + new WebviewHost(platform, std::move(webview_env3))); + } + } + } + + return {}; +} + +WebviewHost::WebviewHost(WebviewPlatform* platform, + wil::com_ptr webview_env) + : webview_env_(webview_env) { + compositor_ = platform->graphics_context()->CreateCompositor(); +} + +void WebviewHost::CreateWebview(HWND hwnd, bool offscreen_only, + bool owns_window, + WebviewCreationCallback callback) { + CreateWebViewCompositionController( + hwnd, [=, self = this]( + wil::com_ptr controller, + std::unique_ptr error) { + if (controller) { + std::unique_ptr webview(new Webview( + std::move(controller), self, hwnd, owns_window, offscreen_only)); + callback(std::move(webview), nullptr); + } else { + callback(nullptr, std::move(error)); + } + }); +} + +void WebviewHost::CreateWebViewPointerInfo(PointerInfoCreationCallback callback) { + + ICoreWebView2PointerInfo *pointer; + auto hr = webview_env_->CreateCoreWebView2PointerInfo(&pointer); + + if (FAILED(hr)) { + callback(nullptr, WebviewCreationError::create(hr, "CreateWebViewPointerInfo failed.")); + } else if (SUCCEEDED(hr)) { + callback(std::move(wil::com_ptr(pointer)), nullptr); + } +} + +void WebviewHost::CreateWebViewCompositionController( + HWND hwnd, CompositionControllerCreationCallback callback) { + auto hr = webview_env_->CreateCoreWebView2CompositionController( + hwnd, + Callback< + ICoreWebView2CreateCoreWebView2CompositionControllerCompletedHandler>( + [callback](HRESULT hr, + ICoreWebView2CompositionController* compositionController) + -> HRESULT { + if (SUCCEEDED(hr)) { + callback( + std::move(wil::com_ptr( + compositionController)), + nullptr); + } else { + callback(nullptr, WebviewCreationError::create( + hr, + "CreateCoreWebView2CompositionController " + "completion handler failed.")); + } + + return S_OK; + }) + .Get()); + + if (FAILED(hr)) { + callback(nullptr, + WebviewCreationError::create( + hr, "CreateCoreWebView2CompositionController failed.")); + } +} diff --git a/fdGamer/third_party/webview_windows/windows/webview_host.h b/fdGamer/third_party/webview_windows/windows/webview_host.h new file mode 100644 index 00000000..23650246 --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/webview_host.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include + +#include + +#include "graphics_context.h" +#include "webview.h" +#include "webview_platform.h" +#include "windows.ui.composition.h" + +struct WebviewCreationError { + HRESULT hr; + std::string message; + + explicit WebviewCreationError(HRESULT hr, std::string message) + : hr(hr), message(message) {} + + static std::unique_ptr create( + HRESULT hr, const std::string message) { + return std::make_unique(hr, message); + } +}; + +class WebviewHost { + public: + typedef std::function, + std::unique_ptr)> + WebviewCreationCallback; + typedef std::function, + std::unique_ptr)> + CompositionControllerCreationCallback; + typedef std::function, + std::unique_ptr)> + PointerInfoCreationCallback; + + static std::unique_ptr Create( + WebviewPlatform* platform, + std::optional user_data_directory = std::nullopt, + std::optional browser_exe_path = std::nullopt, + std::optional arguments = std::nullopt); + + void CreateWebview(HWND hwnd, bool offscreen_only, bool owns_window, + WebviewCreationCallback callback); + + void CreateWebViewPointerInfo(PointerInfoCreationCallback cb); + + winrt::com_ptr compositor() + const { + return compositor_; + } + + private: + winrt::com_ptr compositor_; + wil::com_ptr webview_env_; + + WebviewHost(WebviewPlatform* platform, + wil::com_ptr webview_env); + void CreateWebViewCompositionController( + HWND hwnd, CompositionControllerCreationCallback cb); +}; diff --git a/fdGamer/third_party/webview_windows/windows/webview_platform.cc b/fdGamer/third_party/webview_windows/windows/webview_platform.cc new file mode 100644 index 00000000..cb26edbc --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/webview_platform.cc @@ -0,0 +1,77 @@ +#include "webview_platform.h" + +#include +#include +#include + +#include +#include + +WebviewPlatform::WebviewPlatform() + : rohelper_(std::make_unique(RO_INIT_SINGLETHREADED)) { + if (rohelper_->WinRtAvailable()) { + DispatcherQueueOptions options{sizeof(DispatcherQueueOptions), + DQTYPE_THREAD_CURRENT, DQTAT_COM_STA}; + + if (FAILED(rohelper_->CreateDispatcherQueueController( + options, dispatcher_queue_controller_.put()))) { + std::cerr << "Creating DispatcherQueueController failed." << std::endl; + return; + } + + if (!IsGraphicsCaptureSessionSupported()) { + std::cerr << "Windows::Graphics::Capture::GraphicsCaptureSession is not " + "supported." + << std::endl; + return; + } + + graphics_context_ = std::make_unique(rohelper_.get()); + valid_ = graphics_context_->IsValid(); + } +} + +bool WebviewPlatform::IsGraphicsCaptureSessionSupported() { + HSTRING className; + HSTRING_HEADER classNameHeader; + + if (FAILED(rohelper_->GetStringReference( + RuntimeClass_Windows_Graphics_Capture_GraphicsCaptureSession, + &className, &classNameHeader))) { + return false; + } + + ABI::Windows::Graphics::Capture::IGraphicsCaptureSessionStatics* + capture_session_statics; + if (FAILED(rohelper_->GetActivationFactory( + className, + __uuidof( + ABI::Windows::Graphics::Capture::IGraphicsCaptureSessionStatics), + (void**)&capture_session_statics))) { + return false; + } + + boolean is_supported = false; + if (FAILED(capture_session_statics->IsSupported(&is_supported))) { + return false; + } + + return !!is_supported; +} + +std::optional WebviewPlatform::GetDefaultDataDirectory() { + PWSTR path_tmp; + if (!SUCCEEDED( + SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &path_tmp))) { + return std::nullopt; + } + auto path = std::filesystem::path(path_tmp); + CoTaskMemFree(path_tmp); + + wchar_t filename[MAX_PATH]; + GetModuleFileName(nullptr, filename, MAX_PATH); + path /= "flutter_webview_windows"; + path /= std::filesystem::path(filename).stem(); + + return path.wstring(); +} diff --git a/fdGamer/third_party/webview_windows/windows/webview_platform.h b/fdGamer/third_party/webview_windows/windows/webview_platform.h new file mode 100644 index 00000000..6c4d4585 --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/webview_platform.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +#include +#include +#include + +#include "graphics_context.h" +#include "util/rohelper.h" + +class WebviewPlatform { + public: + WebviewPlatform(); + bool IsSupported() { return valid_; } + std::optional GetDefaultDataDirectory(); + bool IsGraphicsCaptureSessionSupported(); + GraphicsContext* graphics_context() const { + return graphics_context_.get(); + }; + + rx::RoHelper* rohelper() const { return rohelper_.get(); } + + private: + std::unique_ptr rohelper_; + winrt::com_ptr + dispatcher_queue_controller_; + std::unique_ptr graphics_context_; + bool valid_ = false; +}; diff --git a/fdGamer/third_party/webview_windows/windows/webview_windows_plugin.cc b/fdGamer/third_party/webview_windows/windows/webview_windows_plugin.cc new file mode 100644 index 00000000..159b432a --- /dev/null +++ b/fdGamer/third_party/webview_windows/windows/webview_windows_plugin.cc @@ -0,0 +1,246 @@ +#include "include/webview_windows/webview_windows_plugin.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include "webview_bridge.h" +#include "webview_host.h" +#include "webview_platform.h" +#include "util/string_converter.h" + +#pragma comment(lib, "dxgi.lib") +#pragma comment(lib, "d3d11.lib") + +namespace { + +constexpr auto kMethodInitialize = "initialize"; +constexpr auto kMethodDispose = "dispose"; +constexpr auto kMethodInitializeEnvironment = "initializeEnvironment"; +constexpr auto kMethodGetWebViewVersion = "getWebViewVersion"; + +constexpr auto kErrorCodeInvalidId = "invalid_id"; +constexpr auto kErrorCodeEnvironmentCreationFailed = + "environment_creation_failed"; +constexpr auto kErrorCodeEnvironmentAlreadyInitialized = + "environment_already_initialized"; +constexpr auto kErrorCodeWebviewCreationFailed = "webview_creation_failed"; +constexpr auto kErrorUnsupportedPlatform = "unsupported_platform"; + +template +std::optional GetOptionalValue(const flutter::EncodableMap& map, + const std::string& key) { + const auto it = map.find(flutter::EncodableValue(key)); + if (it != map.end()) { + const auto val = std::get_if(&it->second); + if (val) { + return *val; + } + } + return std::nullopt; +} + +class WebviewWindowsPlugin : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); + + WebviewWindowsPlugin(flutter::TextureRegistrar* textures, + flutter::BinaryMessenger* messenger); + + virtual ~WebviewWindowsPlugin(); + + private: + std::unique_ptr platform_; + std::unique_ptr webview_host_; + std::unordered_map> instances_; + + WNDCLASS window_class_ = {}; + flutter::TextureRegistrar* textures_; + flutter::BinaryMessenger* messenger_; + + bool InitPlatform(); + + void CreateWebviewInstance( + std::unique_ptr>); + // Called when a method is called on this plugin's channel from Dart. + void HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result); +}; + +// static +void WebviewWindowsPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarWindows* registrar) { + auto channel = + std::make_unique>( + registrar->messenger(), "io.jns.webview.win", + &flutter::StandardMethodCodec::GetInstance()); + + auto plugin = std::make_unique( + registrar->texture_registrar(), registrar->messenger()); + + channel->SetMethodCallHandler( + [plugin_pointer = plugin.get()](const auto& call, auto result) { + plugin_pointer->HandleMethodCall(call, std::move(result)); + }); + + registrar->AddPlugin(std::move(plugin)); +} + +WebviewWindowsPlugin::WebviewWindowsPlugin(flutter::TextureRegistrar* textures, + flutter::BinaryMessenger* messenger) + : textures_(textures), messenger_(messenger) { + window_class_.lpszClassName = L"FlutterWebviewMessage"; + window_class_.lpfnWndProc = &DefWindowProc; + RegisterClass(&window_class_); +} + +WebviewWindowsPlugin::~WebviewWindowsPlugin() { + instances_.clear(); + UnregisterClass(window_class_.lpszClassName, nullptr); +} + +void WebviewWindowsPlugin::HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result) { + if (method_call.method_name().compare(kMethodInitializeEnvironment) == 0) { + if (webview_host_) { + return result->Error(kErrorCodeEnvironmentAlreadyInitialized, + "The webview environment is already initialized"); + } + + if (!InitPlatform()) { + return result->Error(kErrorUnsupportedPlatform, + "The platform is not supported"); + } + + const auto& map = std::get(*method_call.arguments()); + + std::optional browser_exe_wpath = std::nullopt; + std::optional browser_exe_path = + GetOptionalValue(map, "browserExePath"); + if (browser_exe_path) { + browser_exe_wpath = util::Utf16FromUtf8(*browser_exe_path); + } + + std::optional user_data_wpath = std::nullopt; + std::optional user_data_path = + GetOptionalValue(map, "userDataPath"); + if (user_data_path) { + user_data_wpath = util::Utf16FromUtf8(*user_data_path); + } else { + user_data_wpath = platform_->GetDefaultDataDirectory(); + } + + std::optional additional_args = + GetOptionalValue(map, "additionalArguments"); + + webview_host_ = std::move(WebviewHost::Create( + platform_.get(), user_data_wpath, browser_exe_wpath, additional_args)); + if (!webview_host_) { + return result->Error(kErrorCodeEnvironmentCreationFailed); + } + + return result->Success(); + } + + if (method_call.method_name().compare(kMethodGetWebViewVersion) == 0) { + LPWSTR version_info = nullptr; + auto hr = GetAvailableCoreWebView2BrowserVersionString(nullptr, &version_info); + if (SUCCEEDED(hr) && version_info != nullptr) { + return result->Success(flutter::EncodableValue(util::Utf8FromUtf16(version_info))); + } else { + return result->Success(); + } + } + + if (method_call.method_name().compare(kMethodInitialize) == 0) { + return CreateWebviewInstance(std::move(result)); + } + + if (method_call.method_name().compare(kMethodDispose) == 0) { + if (const auto texture_id = std::get_if(method_call.arguments())) { + const auto it = instances_.find(*texture_id); + if (it != instances_.end()) { + instances_.erase(it); + return result->Success(); + } + } + return result->Error(kErrorCodeInvalidId); + } else { + result->NotImplemented(); + } +} + +void WebviewWindowsPlugin::CreateWebviewInstance( + std::unique_ptr> result) { + if (!InitPlatform()) { + return result->Error(kErrorUnsupportedPlatform, + "The platform is not supported"); + } + + if (!webview_host_) { + webview_host_ = std::move(WebviewHost::Create( + platform_.get(), platform_->GetDefaultDataDirectory())); + if (!webview_host_) { + return result->Error(kErrorCodeEnvironmentCreationFailed); + } + } + + auto hwnd = CreateWindowEx(0, window_class_.lpszClassName, L"", 0, CW_DEFAULT, + CW_DEFAULT, 0, 0, HWND_MESSAGE, nullptr, + window_class_.hInstance, nullptr); + + std::shared_ptr> + shared_result = std::move(result); + webview_host_->CreateWebview( + hwnd, true, true, + [shared_result, this](std::unique_ptr webview, + std::unique_ptr error) { + if (!webview) { + if (error) { + return shared_result->Error( + kErrorCodeWebviewCreationFailed, + std::format( + "Creating the webview failed: {} (HRESULT: {:#010x})", + error->message, error->hr)); + } + return shared_result->Error(kErrorCodeWebviewCreationFailed, + "Creating the webview failed."); + } + + auto bridge = std::make_unique( + messenger_, textures_, platform_->graphics_context(), + std::move(webview)); + auto texture_id = bridge->texture_id(); + instances_[texture_id] = std::move(bridge); + + auto response = flutter::EncodableValue(flutter::EncodableMap{ + {flutter::EncodableValue("textureId"), + flutter::EncodableValue(texture_id)}, + }); + + shared_result->Success(response); + }); +} + +bool WebviewWindowsPlugin::InitPlatform() { + if (!platform_) { + platform_ = std::make_unique(); + } + return platform_->IsSupported(); +} + +} // namespace + +void WebviewWindowsPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + WebviewWindowsPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} diff --git a/fdGamer/tools/nuget.exe b/fdGamer/tools/nuget.exe new file mode 100644 index 00000000..0e535e73 Binary files /dev/null and b/fdGamer/tools/nuget.exe differ diff --git a/fdGamer/windows/.gitignore b/fdGamer/windows/.gitignore new file mode 100644 index 00000000..d492d0d9 --- /dev/null +++ b/fdGamer/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/fdGamer/windows/CMakeLists.txt b/fdGamer/windows/CMakeLists.txt new file mode 100644 index 00000000..a577e6ae --- /dev/null +++ b/fdGamer/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(floating_mini_games LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "floating_mini_games") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/fdGamer/windows/flutter/CMakeLists.txt b/fdGamer/windows/flutter/CMakeLists.txt new file mode 100644 index 00000000..903f4899 --- /dev/null +++ b/fdGamer/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/fdGamer/windows/flutter/generated_plugin_registrant.cc b/fdGamer/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..ae100296 --- /dev/null +++ b/fdGamer/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,20 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); + WebviewWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WebviewWindowsPlugin")); + WindowManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowManagerPlugin")); +} diff --git a/fdGamer/windows/flutter/generated_plugin_registrant.h b/fdGamer/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..dc139d85 --- /dev/null +++ b/fdGamer/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/fdGamer/windows/flutter/generated_plugins.cmake b/fdGamer/windows/flutter/generated_plugins.cmake new file mode 100644 index 00000000..172698f3 --- /dev/null +++ b/fdGamer/windows/flutter/generated_plugins.cmake @@ -0,0 +1,26 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + screen_retriever_windows + webview_windows + window_manager +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/fdGamer/windows/runner/CMakeLists.txt b/fdGamer/windows/runner/CMakeLists.txt new file mode 100644 index 00000000..394917c0 --- /dev/null +++ b/fdGamer/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/fdGamer/windows/runner/Runner.rc b/fdGamer/windows/runner/Runner.rc new file mode 100644 index 00000000..d5ffded1 --- /dev/null +++ b/fdGamer/windows/runner/Runner.rc @@ -0,0 +1,119 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include \"\"winres.h\"\"\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "Bansonic" "\0" + VALUE "FileDescription", "浮动小游戏" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "floating_mini_games" "\0" + VALUE "LegalCopyright", "Copyright (C) 2026 Bansonic. All rights reserved." "\0" + VALUE "OriginalFilename", "浮动小游戏.exe" "\0" + VALUE "ProductName", "浮动小游戏" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/fdGamer/windows/runner/flutter_window.cpp b/fdGamer/windows/runner/flutter_window.cpp new file mode 100644 index 00000000..955ee303 --- /dev/null +++ b/fdGamer/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/fdGamer/windows/runner/flutter_window.h b/fdGamer/windows/runner/flutter_window.h new file mode 100644 index 00000000..6da0652f --- /dev/null +++ b/fdGamer/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/fdGamer/windows/runner/main.cpp b/fdGamer/windows/runner/main.cpp new file mode 100644 index 00000000..cf64b2db --- /dev/null +++ b/fdGamer/windows/runner/main.cpp @@ -0,0 +1,36 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t* command_line, _In_ int show_command) { + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + std::vector command_line_arguments = GetCommandLineArguments(); + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"\u6d6e\u52a8\u5c0f\u6e38\u620f", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/fdGamer/windows/runner/resource.h b/fdGamer/windows/runner/resource.h new file mode 100644 index 00000000..66a65d1e --- /dev/null +++ b/fdGamer/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/fdGamer/windows/runner/resources/app_icon.ico b/fdGamer/windows/runner/resources/app_icon.ico new file mode 100644 index 00000000..e663bb27 Binary files /dev/null and b/fdGamer/windows/runner/resources/app_icon.ico differ diff --git a/fdGamer/windows/runner/runner.exe.manifest b/fdGamer/windows/runner/runner.exe.manifest new file mode 100644 index 00000000..153653e8 --- /dev/null +++ b/fdGamer/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/fdGamer/windows/runner/utils.cpp b/fdGamer/windows/runner/utils.cpp new file mode 100644 index 00000000..3a0b4651 --- /dev/null +++ b/fdGamer/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/fdGamer/windows/runner/utils.h b/fdGamer/windows/runner/utils.h new file mode 100644 index 00000000..3879d547 --- /dev/null +++ b/fdGamer/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/fdGamer/windows/runner/win32_window.cpp b/fdGamer/windows/runner/win32_window.cpp new file mode 100644 index 00000000..60608d0f --- /dev/null +++ b/fdGamer/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/fdGamer/windows/runner/win32_window.h b/fdGamer/windows/runner/win32_window.h new file mode 100644 index 00000000..e901dde6 --- /dev/null +++ b/fdGamer/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_