diff --git a/.cirrus.yml b/.cirrus.yml index 0669c3142b443..ace33146a041c 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -74,4 +74,4 @@ task: bin/flutter analyze --dartdocs --flutter-repo --local-engine=host_debug_unopt test_framework_script: | cd $FRAMEWORK_PATH/flutter/packages/flutter - ../../bin/flutter test --local-engine=host_debug_unopt + ../../bin/flutter test --local-engine=host_debug_unopt --null-assertions --sound-null-safety --enable-experiment=non-nullable diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9aab6bc2b8d1b..551401f5e4932 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -32,6 +32,12 @@ Before you create this PR confirm that it meets all requirements listed below by - [ ] All existing and new tests are passing. - [ ] I am willing to follow-up on review comments in a timely manner. + +## Reviewer Checklist + +- [ ] I have submitted any presubmit flakes in this PR using the [engine presubmit flakes form] before re-triggering the failure. + + ## Breaking Change Did any tests fail when you ran them? Please read [handling breaking changes]. @@ -50,3 +56,4 @@ Did any tests fail when you ran them? Please read [handling breaking changes]. [CLA]: https://cla.developers.google.com/ [tree hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [handling breaking changes]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes +[engine presubmit flakes form]: https://forms.gle/Wc1VyFRYJjQTH6w5A diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml deleted file mode 100644 index 8a18797fd9d6e..0000000000000 --- a/.github/auto_assign.yml +++ /dev/null @@ -1,38 +0,0 @@ -# This is the config file for `auto-assign` bot. -# https://github.com/kentaro-m/auto-assign/ - -# Set to true to add reviewers to pull requests -addReviewers: true - -# Set to true to add assignees to pull requests -addAssignees: false - -# A list of reviewers to be added to pull requests (GitHub user name) -# Note: Add new engine contributors here when joining the team. -reviewers: - - gaaclarke - - liyuqian - - gw280 - - chinmaygarde - - GaryQian - - jason-simmons - - iskakaushik - - flar - -# A number of reviewers added to the pull request -# Set 0 to add all the reviewers (default: 0) -numberOfReviewers: 1 - -# A list of assignees, overrides reviewers if set -# assignees: -# - assigneeA - -# A number of assignees to add to the pull request -# Set to 0 to add all of the assignees. -# Uses numberOfReviewers if unset. -# numberOfAssignees: 2 - -# A list of keywords to be skipped the process that add reviewers if pull requests include it -skipKeywords: - - Roll - - web diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 86c46f4b65e33..4cd3c378af349 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,11 +20,18 @@ contributing guide. The Flutter engine follows Google style for the languages it uses: - [C++](https://google.github.io/styleguide/cppguide.html) + - **Note**: The Linux embedding generally follows idiomatic GObject-based C style. + Use of C++ is discouraged in that embedding to avoid creating hybrid code that + feels unfamiliar to either developers used to working with GObject or C++ developers. + E.g., do not use STL collections or std::string. Exceptions: + - C-style casts are forbidden; use C++ casts. + - Use `nullptr` rather than `NULL`. + - Avoid `#define`; for internal constants use `static constexpr` instead. - [Objective-C](https://google.github.io/styleguide/objcguide.html) (including [Objective-C++](https://google.github.io/styleguide/objcguide.html#objective-c)) - [Java](https://google.github.io/styleguide/javaguide.html) -C++ and Objective-C/C++ files are formatted with `clang-format`, and GN files with `gn format`. +C/C++ and Objective-C/C++ files are formatted with `clang-format`, and GN files with `gn format`. [build_status]: https://cirrus-ci.com/github/flutter/engine [code_of_conduct]: https://github.com/flutter/flutter/blob/master/CODE_OF_CONDUCT.md diff --git a/DEPS b/DEPS index beaa356bb7712..e912065eec6da 100644 --- a/DEPS +++ b/DEPS @@ -26,7 +26,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', # OCMock is for testing only so there is no google clone 'ocmock_git': 'https://github.com/erikdoe/ocmock.git', - 'skia_revision': 'a7f69c290667bb969788e014e3308cef6e187740', + 'skia_revision': '5c7bb326a7b3015d36a86094f54e272c787f721b', # When updating the Dart revision, ensure that all entries that are # dependencies of Dart are also updated to match the entries in the @@ -34,39 +34,39 @@ vars = { # Dart is: https://github.com/dart-lang/sdk/blob/master/DEPS. # You can use //tools/dart/create_updated_flutter_deps.py to produce # updated revision list of existing dependencies. - 'dart_revision': '9560a32779fc966349dd2d719b76aee49fb6a1de', + 'dart_revision': 'a3d902d8598eabf68db42c6f7911fb7fb63f7012', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py 'dart_args_tag': '1.6.0', 'dart_boringssl_gen_rev': '429ccb1877f7987a6f3988228bc2440e61293499', 'dart_boringssl_rev': '4dfd5af70191b068aebe567b8e29ce108cee85ce', - 'dart_collection_rev': '52e219581f72a3eac013d6f5550c580962677425', + 'dart_collection_rev': '7d44763d62f97698b15c08ee360d838dccb63c88', 'dart_dart_style_tag': '1.3.7', 'dart_http_retry_tag': '0.1.1', 'dart_http_throttle_tag': '1.0.2', 'dart_intl_tag': '0.16.1', - 'dart_linter_tag': '0.1.120', + 'dart_linter_tag': '0.1.121', 'dart_oauth2_tag': '1.6.0', 'dart_protobuf_rev': '3746c8fd3f2b0147623a8e3db89c3ff4330de760', - 'dart_pub_rev': '04e237f78b2302d7f20d0b362554425e8deb8add', + 'dart_pub_rev': 'f0c7771b38155d3829a60d60b5dba2784b100811', 'dart_pub_semver_tag': 'v1.4.4', 'dart_quiver-dart_tag': '246e754fe45cecb6aa5f3f13b4ed61037ff0d784', - 'dart_resource_rev': 'f8e37558a1c4f54550aa463b88a6a831e3e33cd6', + 'dart_resource_rev': '6b79867d0becf5395e5819a75720963b8298e9a7', 'dart_root_certificates_rev': '7e5ec82c99677a2e5b95ce296c4d68b0d3378ed8', 'dart_shelf_packages_handler_tag': '2.0.0', 'dart_shelf_proxy_tag': '0.1.0+7', 'dart_shelf_static_rev': 'v0.2.8', 'dart_shelf_web_socket_tag': '0.2.2+3', 'dart_sse_tag': 'e5cf68975e8e87171a3dc297577aa073454a91dc', - 'dart_stack_trace_tag': 'a958966148516dfa64e2b54c14492175da5cc8e1', + 'dart_stack_trace_tag': '45319bfd2a6da228d8c32b06e1da02ad199373c7', 'dart_stagehand_tag': 'v3.3.9', 'dart_stream_channel_tag': 'c446774fd077c9bdbd6235a7aadc661ef60a9727', 'dart_test_reflective_loader_tag': '0.1.9', - 'dart_tflite_native_rev': '3c777c40608a2a9f1427bfe0028ab48e7116b4c1', + 'dart_tflite_native_rev': '0.4.0+1', 'dart_typed_data_tag': 'f94fc57b8e8c0e4fe4ff6cfd8290b94af52d3719', 'dart_usage_tag': '3.4.0', - 'dart_watcher_rev': 'fc3c9aae5d31d707b3013b42634dde8d8a1161b4', + 'dart_watcher_rev': '5df2e364b0c3ec12b9ed9cf2eedb71f9ddf8b7cd', 'ocmock_tag': 'v3.7.1', @@ -105,7 +105,7 @@ allowed_hosts = [ ] deps = { - 'src': 'https://github.com/flutter/buildroot.git' + '@' + 'f83d1d75216e97fb696434bca1cb9a4e7a570fb6', + 'src': 'https://github.com/flutter/buildroot.git' + '@' + '9184ff0695be1b3e4bb20cf64efcfa56daa0a3c0', # Fuchsia compatibility # @@ -143,7 +143,7 @@ deps = { Var('chromium_git') + '/chromium/src/ios.git' + '@' + Var('ios_tools_revision'), 'src/third_party/icu': - Var('chromium_git') + '/chromium/deps/icu.git' + '@' + '8d29692df640668ed7e4d1817715440c4e05697a', + Var('chromium_git') + '/chromium/deps/icu.git' + '@' + '146cb611fb2c1f53e63c2e59bd735d7a8ac6ec8c', 'src/third_party/khronos': Var('chromium_git') + '/chromium/src/third_party/khronos.git' + '@' + '7122230e90547962e0f0c627f62eeed3c701f275', @@ -175,7 +175,7 @@ deps = { Var('dart_git') + '/charcode.git@4a685faba42d86ebd9d661eadd1e79d0a1c34c43', 'src/third_party/dart/third_party/pkg/cli_util': - Var('dart_git') + '/cli_util.git@0.2.0', + Var('dart_git') + '/cli_util.git@335ed165887d0ec97c2a09173ebf22dcf56a6c4e', 'src/third_party/dart/third_party/pkg/collection': Var('dart_git') + '/collection.git' + '@' + Var('dart_collection_rev'), @@ -193,10 +193,10 @@ deps = { Var('dart_git') + '/dart2js_info.git@0632a623b08e1f601c7eba99e0186a581ae799e9', 'src/third_party/dart/third_party/pkg/dartdoc': - Var('dart_git') + '/dartdoc.git@2bef0f260594b822f55c8c8f777d9c4c1ea8f76c', + Var('dart_git') + '/dartdoc.git@8f5f30e58bbc0f11f104888ee87f11cbd6b82cc7', 'src/third_party/dart/third_party/pkg/ffi': - Var('dart_git') + '/ffi.git@454ab0f9ea6bd06942a983238d8a6818b1357edb', + Var('dart_git') + '/ffi.git@a90bd424116fb6f416337db67425171f2dc4c98f', 'src/third_party/dart/third_party/pkg/fixnum': Var('dart_git') + '/fixnum.git@16d3890c6dc82ca629659da1934e412292508bba', @@ -208,13 +208,13 @@ deps = { Var('dart_git') + '/html.git@22f17e97fedeacaa1e945cf84d8016284eed33a6', 'src/third_party/dart/third_party/pkg/http': - Var('dart_git') + '/http.git@ca418355b5fc60cf981de3bd7364ec0dd943fa8f', + Var('dart_git') + '/http.git@20e3a594872ae1565249c7cfc8977f9a10ca9927', 'src/third_party/dart/third_party/pkg/http_multi_server': Var('dart_git') + '/http_multi_server.git@ea269f79321d659208402088f3297e8920a88ee6', 'src/third_party/dart/third_party/pkg/http_parser': - Var('dart_git') + '/http_parser.git@6e63a97b5aaa2b4d1215fe01683e51fb73258e54', + Var('dart_git') + '/http_parser.git@5dd4d16693242049dfb43b5efa429fedbf932e98', 'src/third_party/dart/third_party/pkg/http_retry': Var('dart_git') + '/http_retry.git' + '@' + Var('dart_http_retry_tag'), @@ -232,10 +232,10 @@ deps = { Var('dart_git') + '/linter.git' + '@' + Var('dart_linter_tag'), 'src/third_party/dart/third_party/pkg/logging': - Var('dart_git') + '/logging.git@1590ba0b648a51e7eb3895c612e4b72f72623b6f', + Var('dart_git') + '/logging.git@9d2a7fdd05b09bc06474881152b5baaf38fd1329', 'src/third_party/dart/third_party/pkg/markdown': - Var('dart_git') + '/markdown.git@dbeafd47759e7dd0a167602153bb9c49fb5e5fe7', + Var('dart_git') + '/markdown.git@6f89681d59541ddb1cf3a58efbdaa2304ffc3f51', 'src/third_party/dart/third_party/pkg/matcher': Var('dart_git') + '/matcher.git@9cae8faa7868bf3a88a7ba45eb0bd128e66ac515', @@ -349,7 +349,7 @@ deps = { Var('dart_git') + '/package_config.git@9c586d04bd26fef01215fd10e7ab96a3050cfa64', 'src/third_party/dart/tools/sdks': - {'packages': [{'version': 'version:2.10.0-110.3.beta', 'package': 'dart/dart-sdk/${{platform}}'}], 'dep_type': 'cipd'}, + {'packages': [{'version': 'version:2.11.0-190.0.dev', 'package': 'dart/dart-sdk/${{platform}}'}], 'dep_type': 'cipd'}, # WARNING: end of dart dependencies list that is cleaned up automatically - see create_updated_flutter_deps.py. @@ -522,7 +522,7 @@ deps = { 'packages': [ { 'package': 'fuchsia/sdk/core/mac-amd64', - 'version': 'm6w8tDXMmHQL489ST4B5AUN5EgLI4SfaptN1uq7rHk4C' + 'version': 'WLxBkBnZaNoU9jUg-hJISRzU7ypNG7hHa5RaOKH3dx4C' } ], 'condition': 'host_os == "mac"', @@ -542,7 +542,7 @@ deps = { 'packages': [ { 'package': 'fuchsia/sdk/core/linux-amd64', - 'version': 'kr1tNtZvZ94LI14n82Mp5cR_xcAMiUcoCSJoCEkHPDsC' + 'version': 'gqS_DIjN4Qc6rrMPDMuxaVmIfaJwUEH2ofE363gthpwC' } ], 'condition': 'host_os == "linux"', diff --git a/assets/asset_manager.cc b/assets/asset_manager.cc index 60d169a31ebb2..0fbc28702e9f7 100644 --- a/assets/asset_manager.cc +++ b/assets/asset_manager.cc @@ -29,6 +29,10 @@ void AssetManager::PushBack(std::unique_ptr resolver) { resolvers_.push_back(std::move(resolver)); } +std::deque> AssetManager::TakeResolvers() { + return std::move(resolvers_); +} + // |AssetResolver| std::unique_ptr AssetManager::GetAsMapping( const std::string& asset_name) const { @@ -52,4 +56,9 @@ bool AssetManager::IsValid() const { return resolvers_.size() > 0; } +// |AssetResolver| +bool AssetManager::IsValidAfterAssetManagerChange() const { + return false; +} + } // namespace flutter diff --git a/assets/asset_manager.h b/assets/asset_manager.h index 2278742f50113..0a0f0ff170d4c 100644 --- a/assets/asset_manager.h +++ b/assets/asset_manager.h @@ -25,9 +25,14 @@ class AssetManager final : public AssetResolver { void PushBack(std::unique_ptr resolver); + std::deque> TakeResolvers(); + // |AssetResolver| bool IsValid() const override; + // |AssetResolver| + bool IsValidAfterAssetManagerChange() const override; + // |AssetResolver| std::unique_ptr GetAsMapping( const std::string& asset_name) const override; diff --git a/assets/asset_resolver.h b/assets/asset_resolver.h index b6cdb88b84ac0..8b3e323218939 100644 --- a/assets/asset_resolver.h +++ b/assets/asset_resolver.h @@ -21,6 +21,24 @@ class AssetResolver { virtual bool IsValid() const = 0; + //---------------------------------------------------------------------------- + /// @brief Certain asset resolvers are still valid after the asset + /// manager is replaced before a hot reload, or after a new run + /// configuration is created during a hot restart. By preserving + /// these resolvers and re-inserting them into the new resolver or + /// run configuration, the tooling can avoid needing to sync all + /// application assets through the Dart devFS upon connecting to + /// the VM Service. Besides improving the startup performance of + /// running a Flutter application, it also reduces the occurance + /// of tool failures due to repeated network flakes caused by + /// damaged cables or hereto unknown bugs in the Dart HTTP server + /// implementation. + /// + /// @return Returns whether this resolver is valid after the asset manager + /// or run configuration is updated. + /// + virtual bool IsValidAfterAssetManagerChange() const = 0; + [[nodiscard]] virtual std::unique_ptr GetAsMapping( const std::string& asset_name) const = 0; diff --git a/assets/directory_asset_bundle.cc b/assets/directory_asset_bundle.cc index 5ad7297313c99..d15a7e7373c15 100644 --- a/assets/directory_asset_bundle.cc +++ b/assets/directory_asset_bundle.cc @@ -12,11 +12,14 @@ namespace flutter { -DirectoryAssetBundle::DirectoryAssetBundle(fml::UniqueFD descriptor) +DirectoryAssetBundle::DirectoryAssetBundle( + fml::UniqueFD descriptor, + bool is_valid_after_asset_manager_change) : descriptor_(std::move(descriptor)) { if (!fml::IsDirectory(descriptor_)) { return; } + is_valid_after_asset_manager_change_ = is_valid_after_asset_manager_change; is_valid_ = true; } @@ -27,6 +30,11 @@ bool DirectoryAssetBundle::IsValid() const { return is_valid_; } +// |AssetResolver| +bool DirectoryAssetBundle::IsValidAfterAssetManagerChange() const { + return is_valid_after_asset_manager_change_; +} + // |AssetResolver| std::unique_ptr DirectoryAssetBundle::GetAsMapping( const std::string& asset_name) const { diff --git a/assets/directory_asset_bundle.h b/assets/directory_asset_bundle.h index 0a0a94c7aba15..49b02cdd27c71 100644 --- a/assets/directory_asset_bundle.h +++ b/assets/directory_asset_bundle.h @@ -14,17 +14,22 @@ namespace flutter { class DirectoryAssetBundle : public AssetResolver { public: - explicit DirectoryAssetBundle(fml::UniqueFD descriptor); + DirectoryAssetBundle(fml::UniqueFD descriptor, + bool is_valid_after_asset_manager_change); ~DirectoryAssetBundle() override; private: const fml::UniqueFD descriptor_; bool is_valid_ = false; + bool is_valid_after_asset_manager_change_ = false; // |AssetResolver| bool IsValid() const override; + // |AssetResolver| + bool IsValidAfterAssetManagerChange() const override; + // |AssetResolver| std::unique_ptr GetAsMapping( const std::string& asset_name) const override; diff --git a/ci/analyze.sh b/ci/analyze.sh index fd8d213fd71c2..7c5cc39194c0b 100755 --- a/ci/analyze.sh +++ b/ci/analyze.sh @@ -59,6 +59,7 @@ function analyze() ( ) echo "Analyzing dart:ui library..." +autoninja -C "$SRC_DIR/out/host_debug_unopt" generate_dart_ui analyze \ --options "$FLUTTER_DIR/analysis_options.yaml" \ --enable-experiment=non-nullable \ @@ -79,7 +80,7 @@ analyze \ echo "Analyzing testing/dart..." "$FLUTTER_DIR/tools/gn" --unoptimized -ninja -C "$SRC_DIR/out/host_debug_unopt" sky_engine sky_services +autoninja -C "$SRC_DIR/out/host_debug_unopt" sky_engine sky_services (cd "$FLUTTER_DIR/testing/dart" && "$PUB" get) analyze \ --packages="$FLUTTER_DIR/testing/dart/.dart_tool/package_config.json" \ diff --git a/ci/bin/lint.dart b/ci/bin/lint.dart index 04ff295acd5de..695473265701a 100644 --- a/ci/bin/lint.dart +++ b/ci/bin/lint.dart @@ -11,13 +11,13 @@ import 'dart:async' show Completer; import 'dart:convert' show jsonDecode, utf8, LineSplitter; -import 'dart:io' show File, exit, Directory, FileSystemEntity, Platform, stderr; +import 'dart:io' show File, exit, Directory, FileSystemEntity, Platform, stderr, exitCode; import 'package:args/args.dart'; import 'package:path/path.dart' as path; import 'package:process_runner/process_runner.dart'; -String _linterOutputHeader = ''' +const String _linterOutputHeader = ''' ┌──────────────────────────┐ │ Engine Clang Tidy Linter │ └──────────────────────────┘ @@ -26,6 +26,8 @@ more information on addressing these issues please see: https://github.com/flutter/flutter/wiki/Engine-Clang-Tidy-Linter '''; +const String issueUrlPrefix = 'https://github.com/flutter/flutter/issues'; + class Command { Directory directory = Directory(''); String command = ''; @@ -109,23 +111,59 @@ File buildFileAsRepoFile(String buildFile, Directory repoPath) { return result; } -Future shouldIgnoreFile(File file) async { - if (path.split(file.path).contains('third_party')) { - return 'third_party'; - } else { - final RegExp exp = RegExp(r'//\s*FLUTTER_NOLINT'); - await for (String line - in file.openRead().transform(utf8.decoder).transform(const LineSplitter())) { - if (exp.hasMatch(line)) { - return 'FLUTTER_NOLINT'; - } else if (line.isNotEmpty && line[0] != '\n' && line[0] != '/') { - // Quick out once we find a line that isn't empty or a comment. The - // FLUTTER_NOLINT must show up before the first real code. - return ''; - } +/// Lint actions to apply to a file. +enum LintAction { + /// Run the linter over the file. + lint, + + /// Ignore files under third_party/. + skipThirdParty, + + /// Ignore due to a well-formed FLUTTER_NOLINT comment. + skipNoLint, + + /// Fail due to a malformed FLUTTER_NOLINT comment. + failMalformedNoLint, +} + +bool isThirdPartyFile(File file) { + return path.split(file.path).contains('third_party'); +} + +Future getLintAction(File file) async { + if (isThirdPartyFile(file)) { + return LintAction.skipThirdParty; + } + + // Check for FlUTTER_NOLINT at top of file. + final RegExp exp = RegExp('\/\/\\s*FLUTTER_NOLINT(: $issueUrlPrefix/\\d+)?'); + final Stream lines = file.openRead() + .transform(utf8.decoder) + .transform(const LineSplitter()); + await for (String line in lines) { + final RegExpMatch match = exp.firstMatch(line); + if (match != null) { + return match.group(1) != null + ? LintAction.skipNoLint + : LintAction.failMalformedNoLint; + } else if (line.isNotEmpty && line[0] != '\n' && line[0] != '/') { + // Quick out once we find a line that isn't empty or a comment. The + // FLUTTER_NOLINT must show up before the first real code. + return LintAction.lint; } - return ''; } + return LintAction.lint; +} + +WorkerJob createLintJob(Command command, String checks, String tidyPath) { + final String tidyArgs = calcTidyArgs(command); + final List args = [command.file.path, checks, '--']; + args.addAll(tidyArgs?.split(' ') ?? []); + return WorkerJob( + [tidyPath, ...args], + workingDirectory: command.directory, + name: 'clang-tidy on ${command.file.path}', + ); } void _usage(ArgParser parser, {int exitCode = 1}) { @@ -223,38 +261,48 @@ void main(List arguments) async { 'commands associated with them and can be lint checked.'); } - int exitCode = 0; final List jobs = []; for (Command command in changedFileBuildCommands) { final String relativePath = path.relative(command.file.path, from: repoPath.parent.path); - final String ignoreReason = await shouldIgnoreFile(command.file); - if (ignoreReason.isEmpty) { - final String tidyArgs = calcTidyArgs(command); - final List args = [command.file.path, checks, '--']; - args.addAll(tidyArgs?.split(' ') ?? []); - print('🔶 linting $relativePath'); - jobs.add(WorkerJob( - [tidyPath, ...args], - workingDirectory: command.directory, - name: 'clang-tidy on ${command.file.path}', - )); - } else { - print('🔷 ignoring $relativePath ($ignoreReason)'); + final LintAction action = await getLintAction(command.file); + switch (action) { + case LintAction.skipNoLint: + print('🔷 ignoring $relativePath (FLUTTER_NOLINT)'); + break; + case LintAction.failMalformedNoLint: + print('❌ malformed opt-out $relativePath'); + print(' Required format: // FLUTTER_NOLINT: $issueUrlPrefix/ISSUE_ID'); + exitCode = 1; + break; + case LintAction.lint: + print('🔶 linting $relativePath'); + jobs.add(createLintJob(command, checks, tidyPath)); + break; + case LintAction.skipThirdParty: + print('🔷 ignoring $relativePath (third_party)'); + break; } } final ProcessPool pool = ProcessPool(); await for (final WorkerJob job in pool.startWorkers(jobs)) { - if (job.result?.stdout.isEmpty ?? true) { + if (job.result?.exitCode == 0) { continue; } - print('❌ Failures for ${job.name}:'); - print(job.result.stdout); + if (job.result == null) { + print('\n❗ A clang-tidy job failed to run, aborting:\n${job.exception}'); + exitCode = 1; + break; + } else { + print('❌ Failures for ${job.name}:'); + print(job.result.stdout); + } exitCode = 1; } print('\n'); if (exitCode == 0) { print('No lint problems found.'); + } else { + print('Lint problems found.'); } - exit(exitCode); } diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index ed74366279e94..3dca5f1ffbf84 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -426,7 +426,6 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/alarm_clock.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/assets.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/bitmap_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/browser_detection.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/browser_location.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvas_pool.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -463,7 +462,9 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/dom_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/dom_renderer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/engine_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/frame_reference.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/history.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/history/history.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/history/js_url_strategy.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/history/url_strategy.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/backdrop_filter.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/clip.dart @@ -488,7 +489,9 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/recording_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/render_vertices.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/scene.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/scene_builder.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/shader.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/shaders/normalized_gradient.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/shaders/shader.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/shaders/shader_builder.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/surface.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/surface_stats.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/transform.dart @@ -764,6 +767,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugin FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterUiDisplayListener.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/RenderSurface.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/SurfaceTextureWrapper.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/LifecycleChannel.java @@ -1001,6 +1005,8 @@ FILE: ../../../flutter/shell/platform/darwin/ios/ios_external_texture_gl.h FILE: ../../../flutter/shell/platform/darwin/ios/ios_external_texture_gl.mm FILE: ../../../flutter/shell/platform/darwin/ios/ios_external_texture_metal.h FILE: ../../../flutter/shell/platform/darwin/ios/ios_external_texture_metal.mm +FILE: ../../../flutter/shell/platform/darwin/ios/ios_external_view_embedder.h +FILE: ../../../flutter/shell/platform/darwin/ios/ios_external_view_embedder.mm FILE: ../../../flutter/shell/platform/darwin/ios/ios_render_target_gl.h FILE: ../../../flutter/shell/platform/darwin/ios/ios_render_target_gl.mm FILE: ../../../flutter/shell/platform/darwin/ios/ios_surface.h @@ -1509,6 +1515,10 @@ FILE: ../../../flutter/vulkan/vulkan_window.cc FILE: ../../../flutter/vulkan/vulkan_window.h FILE: ../../../flutter/web_sdk/libraries.json FILE: ../../../flutter/web_sdk/sdk_rewriter.dart +FILE: ../../../flutter/web_sdk/web_test_utils/lib/environment.dart +FILE: ../../../flutter/web_sdk/web_test_utils/lib/exceptions.dart +FILE: ../../../flutter/web_sdk/web_test_utils/lib/goldens.dart +FILE: ../../../flutter/web_sdk/web_test_utils/lib/image_compare.dart ---------------------------------------------------------------------------------------------------- Copyright 2013 The Flutter Authors. All rights reserved. diff --git a/ci/licenses_golden/licenses_fuchsia b/ci/licenses_golden/licenses_fuchsia index 50f5061254d22..f480874cebe0f 100644 --- a/ci/licenses_golden/licenses_fuchsia +++ b/ci/licenses_golden/licenses_fuchsia @@ -1,4 +1,4 @@ -Signature: 6aa5794c7bfd21165215fadf82cc2df3 +Signature: 5b0fa6a6aa38eb8ea2a8fbe77daa8bc3 UNUSED LICENSES: @@ -926,6 +926,7 @@ FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/zircon/exception.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/zircon/syscalls/clock.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/zircon/syscalls/internal/cdecls.inc FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/zircon/syscalls/scheduler.h +FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/zircon/utc.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/lib/Scrt1.o FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/lib/libc.so FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/lib/libdl.so @@ -1133,6 +1134,7 @@ FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/zircon/exception.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/zircon/syscalls/clock.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/zircon/syscalls/internal/cdecls.inc FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/zircon/syscalls/scheduler.h +FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/zircon/utc.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/lib/Scrt1.o FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/lib/libc.so FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/lib/libdl.so @@ -2508,6 +2510,7 @@ FILE: ../../../fuchsia/sdk/linux/dart/fidl/lib/src/message.dart FILE: ../../../fuchsia/sdk/linux/dart/fidl/lib/src/struct.dart FILE: ../../../fuchsia/sdk/linux/dart/fidl/lib/src/table.dart FILE: ../../../fuchsia/sdk/linux/dart/fidl/lib/src/types.dart +FILE: ../../../fuchsia/sdk/linux/dart/fidl/lib/src/unknown_data.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia/lib/src/fakes/fuchsia_fakes.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_logger/lib/logger.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_logger/lib/src/internal/_fuchsia_log_writer.dart @@ -3222,6 +3225,7 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.weave/common.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.weave/weavestack.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.wlan.product.deprecatedconfiguration/wlan_deprecated_configuration.fidl FILE: ../../../fuchsia/sdk/linux/fidl/zx/zx_common.fidl +FILE: ../../../fuchsia/sdk/linux/pkg/fidl_base/handle_close_many.cc FILE: ../../../fuchsia/sdk/linux/pkg/fidl_base/include/lib/fidl/trace.h FILE: ../../../fuchsia/sdk/linux/pkg/fidl_base/validate_string.cc FILE: ../../../fuchsia/sdk/linux/pkg/fidl_cpp/include/lib/fidl/cpp/internal/bitset.h diff --git a/ci/licenses_golden/licenses_skia b/ci/licenses_golden/licenses_skia index 07737c30603da..10532fd96951b 100644 --- a/ci/licenses_golden/licenses_skia +++ b/ci/licenses_golden/licenses_skia @@ -1,4 +1,4 @@ -Signature: d3107608da67165804061970476ba2d7 +Signature: 6e1bc02b61a4bd8a2277bed3487261da UNUSED LICENSES: @@ -435,50 +435,6 @@ FILE: ../../../third_party/skia/bench/StreamBench.cpp FILE: ../../../third_party/skia/bench/SwizzleBench.cpp FILE: ../../../third_party/skia/bench/TileImageFilterBench.cpp FILE: ../../../third_party/skia/bench/VertexColorSpaceBench.cpp -FILE: ../../../third_party/skia/experimental/svg/model/SkPEG.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGAttribute.cpp -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGAttribute.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGAttributeParser.cpp -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGAttributeParser.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGCircle.cpp -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGCircle.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGClipPath.cpp -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGClipPath.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGContainer.cpp -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGContainer.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGDOM.cpp -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGDOM.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGDefs.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGEllipse.cpp -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGEllipse.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGG.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGHiddenContainer.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGIDMapper.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGLine.cpp -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGLine.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGLinearGradient.cpp -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGLinearGradient.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGNode.cpp -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGNode.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGPath.cpp -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGPath.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGPoly.cpp -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGPoly.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGRect.cpp -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGRect.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGRenderContext.cpp -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGRenderContext.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGSVG.cpp -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGSVG.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGShape.cpp -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGShape.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGStop.cpp -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGStop.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGTransformableNode.cpp -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGTransformableNode.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGTypes.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGValue.cpp -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGValue.h FILE: ../../../third_party/skia/experimental/xps_to_png/xps_to_png.cs FILE: ../../../third_party/skia/fuzz/Fuzz.cpp FILE: ../../../third_party/skia/fuzz/Fuzz.h @@ -555,6 +511,49 @@ FILE: ../../../third_party/skia/modules/sksg/samples/SampleSVGPong.cpp FILE: ../../../third_party/skia/modules/skshaper/include/SkShaper.h FILE: ../../../third_party/skia/modules/skshaper/src/SkShaper_harfbuzz.cpp FILE: ../../../third_party/skia/modules/skshaper/src/SkShaper_primitive.cpp +FILE: ../../../third_party/skia/modules/svg/include/SkSVGAttribute.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGAttributeParser.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGCircle.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGClipPath.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGContainer.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGDOM.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGDefs.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGEllipse.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGG.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGHiddenContainer.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGIDMapper.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGLine.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGLinearGradient.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGNode.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGPath.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGPoly.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGRect.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGRenderContext.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGSVG.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGShape.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGStop.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGTransformableNode.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGTypes.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGValue.h +FILE: ../../../third_party/skia/modules/svg/src/SkSVGAttribute.cpp +FILE: ../../../third_party/skia/modules/svg/src/SkSVGAttributeParser.cpp +FILE: ../../../third_party/skia/modules/svg/src/SkSVGCircle.cpp +FILE: ../../../third_party/skia/modules/svg/src/SkSVGClipPath.cpp +FILE: ../../../third_party/skia/modules/svg/src/SkSVGContainer.cpp +FILE: ../../../third_party/skia/modules/svg/src/SkSVGDOM.cpp +FILE: ../../../third_party/skia/modules/svg/src/SkSVGEllipse.cpp +FILE: ../../../third_party/skia/modules/svg/src/SkSVGLine.cpp +FILE: ../../../third_party/skia/modules/svg/src/SkSVGLinearGradient.cpp +FILE: ../../../third_party/skia/modules/svg/src/SkSVGNode.cpp +FILE: ../../../third_party/skia/modules/svg/src/SkSVGPath.cpp +FILE: ../../../third_party/skia/modules/svg/src/SkSVGPoly.cpp +FILE: ../../../third_party/skia/modules/svg/src/SkSVGRect.cpp +FILE: ../../../third_party/skia/modules/svg/src/SkSVGRenderContext.cpp +FILE: ../../../third_party/skia/modules/svg/src/SkSVGSVG.cpp +FILE: ../../../third_party/skia/modules/svg/src/SkSVGShape.cpp +FILE: ../../../third_party/skia/modules/svg/src/SkSVGStop.cpp +FILE: ../../../third_party/skia/modules/svg/src/SkSVGTransformableNode.cpp +FILE: ../../../third_party/skia/modules/svg/src/SkSVGValue.cpp FILE: ../../../third_party/skia/samplecode/DecodeFile.h FILE: ../../../third_party/skia/samplecode/Sample.cpp FILE: ../../../third_party/skia/samplecode/SampleAndroidShadows.cpp @@ -619,7 +618,7 @@ FILE: ../../../third_party/skia/src/gpu/GrClipStackClip.cpp FILE: ../../../third_party/skia/src/gpu/GrClipStackClip.h FILE: ../../../third_party/skia/src/gpu/GrColorSpaceXform.cpp FILE: ../../../third_party/skia/src/gpu/GrColorSpaceXform.h -FILE: ../../../third_party/skia/src/gpu/GrContextPriv.h +FILE: ../../../third_party/skia/src/gpu/GrDirectContextPriv.h FILE: ../../../third_party/skia/src/gpu/GrFixedClip.h FILE: ../../../third_party/skia/src/gpu/GrImageTextureMaker.cpp FILE: ../../../third_party/skia/src/gpu/GrImageTextureMaker.h @@ -1284,6 +1283,7 @@ FILE: ../../../third_party/skia/infra/bots/test_skia_bundled.isolate FILE: ../../../third_party/skia/infra/bots/tools/luci-go/linux64/isolate.sha1 FILE: ../../../third_party/skia/infra/bots/tools/luci-go/mac64/isolate.sha1 FILE: ../../../third_party/skia/infra/bots/tools/luci-go/win64/isolate.exe.sha1 +FILE: ../../../third_party/skia/infra/bots/wasm_gm_tests.isolate FILE: ../../../third_party/skia/infra/bots/whole_repo.isolate FILE: ../../../third_party/skia/infra/canvaskit/docker/canvaskit-emsdk/Dockerfile FILE: ../../../third_party/skia/infra/config/recipes.cfg @@ -1307,6 +1307,7 @@ FILE: ../../../third_party/skia/modules/canvaskit/debug.js FILE: ../../../third_party/skia/modules/canvaskit/externs.js FILE: ../../../third_party/skia/modules/canvaskit/font.js FILE: ../../../third_party/skia/modules/canvaskit/fonts/NotoMono-Regular.ttf +FILE: ../../../third_party/skia/modules/canvaskit/gm.js FILE: ../../../third_party/skia/modules/canvaskit/gpu.js FILE: ../../../third_party/skia/modules/canvaskit/helper.js FILE: ../../../third_party/skia/modules/canvaskit/htmlcanvas/_namedcolors.js @@ -1347,6 +1348,8 @@ FILE: ../../../third_party/skia/modules/canvaskit/release.js FILE: ../../../third_party/skia/modules/canvaskit/rt_shader.js FILE: ../../../third_party/skia/modules/canvaskit/skottie.js FILE: ../../../third_party/skia/modules/canvaskit/skp.js +FILE: ../../../third_party/skia/modules/canvaskit/wasm_tools/gmtests.html +FILE: ../../../third_party/skia/modules/canvaskit/wasm_tools/viewer.html FILE: ../../../third_party/skia/modules/pathkit/chaining.js FILE: ../../../third_party/skia/modules/pathkit/externs.js FILE: ../../../third_party/skia/modules/pathkit/helper.js @@ -1691,7 +1694,8 @@ FILE: ../../../third_party/skia/src/effects/SkColorMatrixFilter.cpp FILE: ../../../third_party/skia/src/effects/SkLayerDrawLooper.cpp FILE: ../../../third_party/skia/src/effects/SkPackBits.cpp FILE: ../../../third_party/skia/src/effects/SkTableMaskFilter.cpp -FILE: ../../../third_party/skia/src/gpu/GrContext.cpp +FILE: ../../../third_party/skia/src/gpu/GrAttachment.cpp +FILE: ../../../third_party/skia/src/gpu/GrAttachment.h FILE: ../../../third_party/skia/src/gpu/GrGpu.h FILE: ../../../third_party/skia/src/gpu/GrGpuResource.cpp FILE: ../../../third_party/skia/src/gpu/GrNativeRect.h @@ -1702,14 +1706,14 @@ FILE: ../../../third_party/skia/src/gpu/GrPathRendererChain.cpp FILE: ../../../third_party/skia/src/gpu/GrPathRendererChain.h FILE: ../../../third_party/skia/src/gpu/GrRenderTarget.cpp FILE: ../../../third_party/skia/src/gpu/GrRenderTarget.h -FILE: ../../../third_party/skia/src/gpu/GrStencilAttachment.cpp -FILE: ../../../third_party/skia/src/gpu/GrStencilAttachment.h FILE: ../../../third_party/skia/src/gpu/GrStencilSettings.cpp FILE: ../../../third_party/skia/src/gpu/GrTexture.cpp FILE: ../../../third_party/skia/src/gpu/GrTexture.h FILE: ../../../third_party/skia/src/gpu/SkGpuDevice.cpp FILE: ../../../third_party/skia/src/gpu/geometry/GrPathUtils.cpp FILE: ../../../third_party/skia/src/gpu/geometry/GrPathUtils.h +FILE: ../../../third_party/skia/src/gpu/gl/GrGLAttachment.cpp +FILE: ../../../third_party/skia/src/gpu/gl/GrGLAttachment.h FILE: ../../../third_party/skia/src/gpu/gl/GrGLDefines.h FILE: ../../../third_party/skia/src/gpu/gl/GrGLGLSL.cpp FILE: ../../../third_party/skia/src/gpu/gl/GrGLGLSL.h @@ -1722,8 +1726,6 @@ FILE: ../../../third_party/skia/src/gpu/gl/GrGLProgram.cpp FILE: ../../../third_party/skia/src/gpu/gl/GrGLProgram.h FILE: ../../../third_party/skia/src/gpu/gl/GrGLRenderTarget.cpp FILE: ../../../third_party/skia/src/gpu/gl/GrGLRenderTarget.h -FILE: ../../../third_party/skia/src/gpu/gl/GrGLStencilAttachment.cpp -FILE: ../../../third_party/skia/src/gpu/gl/GrGLStencilAttachment.h FILE: ../../../third_party/skia/src/gpu/gl/GrGLTexture.cpp FILE: ../../../third_party/skia/src/gpu/gl/GrGLTexture.h FILE: ../../../third_party/skia/src/gpu/gl/GrGLUtil.cpp @@ -2552,6 +2554,8 @@ FILE: ../../../third_party/skia/src/gpu/text/GrTextBlob.cpp FILE: ../../../third_party/skia/src/gpu/text/GrTextBlob.h FILE: ../../../third_party/skia/src/gpu/text/GrTextBlobCache.cpp FILE: ../../../third_party/skia/src/gpu/text/GrTextBlobCache.h +FILE: ../../../third_party/skia/src/gpu/vk/GrVkAttachment.cpp +FILE: ../../../third_party/skia/src/gpu/vk/GrVkAttachment.h FILE: ../../../third_party/skia/src/gpu/vk/GrVkBuffer.cpp FILE: ../../../third_party/skia/src/gpu/vk/GrVkBuffer.h FILE: ../../../third_party/skia/src/gpu/vk/GrVkCaps.cpp @@ -2572,8 +2576,6 @@ FILE: ../../../third_party/skia/src/gpu/vk/GrVkRenderPass.cpp FILE: ../../../third_party/skia/src/gpu/vk/GrVkRenderPass.h FILE: ../../../third_party/skia/src/gpu/vk/GrVkRenderTarget.cpp FILE: ../../../third_party/skia/src/gpu/vk/GrVkRenderTarget.h -FILE: ../../../third_party/skia/src/gpu/vk/GrVkStencilAttachment.cpp -FILE: ../../../third_party/skia/src/gpu/vk/GrVkStencilAttachment.h FILE: ../../../third_party/skia/src/gpu/vk/GrVkTexture.cpp FILE: ../../../third_party/skia/src/gpu/vk/GrVkTexture.h FILE: ../../../third_party/skia/src/gpu/vk/GrVkTextureRenderTarget.cpp @@ -2778,7 +2780,6 @@ FILE: ../../../third_party/skia/src/core/SkDrawLooper.cpp FILE: ../../../third_party/skia/src/core/SkFontStream.h FILE: ../../../third_party/skia/src/core/SkGpuBlurUtils.cpp FILE: ../../../third_party/skia/src/core/SkGpuBlurUtils.h -FILE: ../../../third_party/skia/src/core/SkLegacyGpuBlurUtils.cpp FILE: ../../../third_party/skia/src/core/SkMatrixUtils.h FILE: ../../../third_party/skia/src/core/SkMessageBus.h FILE: ../../../third_party/skia/src/core/SkMipmap.cpp @@ -2916,7 +2917,7 @@ FILE: ../../../third_party/skia/modules/particles/src/SkParticleEffect.cpp FILE: ../../../third_party/skia/modules/particles/src/SkReflected.cpp FILE: ../../../third_party/skia/modules/skresources/include/SkResources.h FILE: ../../../third_party/skia/modules/skresources/src/SkResources.cpp -FILE: ../../../third_party/skia/samplecode/SampleBackdropBounds.cpp +FILE: ../../../third_party/skia/samplecode/SampleFilterBounds.cpp FILE: ../../../third_party/skia/samplecode/SampleImageFilterDAG.cpp FILE: ../../../third_party/skia/src/core/SkImageFilterTypes.cpp FILE: ../../../third_party/skia/src/core/SkImageFilterTypes.h @@ -3019,8 +3020,6 @@ FILE: ../../../third_party/skia/experimental/ffmpeg/SkVideoDecoder.cpp FILE: ../../../third_party/skia/experimental/ffmpeg/SkVideoDecoder.h FILE: ../../../third_party/skia/experimental/ffmpeg/SkVideoEncoder.cpp FILE: ../../../third_party/skia/experimental/ffmpeg/SkVideoEncoder.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGText.cpp -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGText.h FILE: ../../../third_party/skia/gm/backdrop.cpp FILE: ../../../third_party/skia/gm/backdrop_imagefilter_croprect.cpp FILE: ../../../third_party/skia/gm/bug9331.cpp @@ -3091,6 +3090,8 @@ FILE: ../../../third_party/skia/modules/sksg/include/SkSGRenderEffect.h FILE: ../../../third_party/skia/modules/sksg/src/SkSGNodePriv.h FILE: ../../../third_party/skia/modules/sksg/src/SkSGRenderEffect.cpp FILE: ../../../third_party/skia/modules/sksg/src/SkSGTransformPriv.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGText.h +FILE: ../../../third_party/skia/modules/svg/src/SkSVGText.cpp FILE: ../../../third_party/skia/samplecode/SampleDegenerateQuads.cpp FILE: ../../../third_party/skia/samplecode/SampleSG.cpp FILE: ../../../third_party/skia/samplecode/SampleThinAA.cpp @@ -3113,12 +3114,12 @@ FILE: ../../../third_party/skia/src/gpu/GrAHardwareBufferUtils.cpp FILE: ../../../third_party/skia/src/gpu/GrAHardwareBufferUtils.h FILE: ../../../third_party/skia/src/gpu/GrBaseContextPriv.h FILE: ../../../third_party/skia/src/gpu/GrBuffer.h -FILE: ../../../third_party/skia/src/gpu/GrContextPriv.cpp FILE: ../../../third_party/skia/src/gpu/GrContextThreadSafeProxy.cpp FILE: ../../../third_party/skia/src/gpu/GrContext_Base.cpp FILE: ../../../third_party/skia/src/gpu/GrCpuBuffer.h FILE: ../../../third_party/skia/src/gpu/GrDataUtils.cpp FILE: ../../../third_party/skia/src/gpu/GrDataUtils.h +FILE: ../../../third_party/skia/src/gpu/GrDirectContextPriv.cpp FILE: ../../../third_party/skia/src/gpu/GrGpuBuffer.cpp FILE: ../../../third_party/skia/src/gpu/GrGpuBuffer.h FILE: ../../../third_party/skia/src/gpu/GrImageContext.cpp @@ -3138,6 +3139,8 @@ FILE: ../../../third_party/skia/src/gpu/ccpr/GrOctoBounds.cpp FILE: ../../../third_party/skia/src/gpu/ccpr/GrOctoBounds.h FILE: ../../../third_party/skia/src/gpu/ccpr/GrStencilAtlasOp.cpp FILE: ../../../third_party/skia/src/gpu/ccpr/GrVSCoverageProcessor.h +FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnAttachment.cpp +FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnAttachment.h FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnBuffer.cpp FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnBuffer.h FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnCaps.cpp @@ -3154,8 +3157,6 @@ FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnRenderTarget.cpp FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnRenderTarget.h FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnRingBuffer.cpp FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnRingBuffer.h -FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnStencilAttachment.cpp -FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnStencilAttachment.h FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnTexture.cpp FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnTexture.h FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnTextureRenderTarget.cpp @@ -3336,7 +3337,6 @@ FILE: ../../../third_party/skia/src/codec/SkEncodedInfo.cpp FILE: ../../../third_party/skia/src/codec/SkParseEncodedOrigin.cpp FILE: ../../../third_party/skia/src/codec/SkWuffsCodec.cpp FILE: ../../../third_party/skia/src/codec/SkWuffsCodec.h -FILE: ../../../third_party/skia/src/core/SkBlurPriv.h FILE: ../../../third_party/skia/src/core/SkCanvasPriv.cpp FILE: ../../../third_party/skia/src/core/SkColorSpaceXformSteps.cpp FILE: ../../../third_party/skia/src/core/SkColorSpaceXformSteps.h @@ -3456,6 +3456,8 @@ FILE: ../../../third_party/skia/src/gpu/gradients/generated/GrTwoPointConicalGra FILE: ../../../third_party/skia/src/gpu/gradients/generated/GrTwoPointConicalGradientLayout.h FILE: ../../../third_party/skia/src/gpu/gradients/generated/GrUnrolledBinaryGradientColorizer.cpp FILE: ../../../third_party/skia/src/gpu/gradients/generated/GrUnrolledBinaryGradientColorizer.h +FILE: ../../../third_party/skia/src/gpu/mtl/GrMtlAttachment.h +FILE: ../../../third_party/skia/src/gpu/mtl/GrMtlAttachment.mm FILE: ../../../third_party/skia/src/gpu/mtl/GrMtlBuffer.h FILE: ../../../third_party/skia/src/gpu/mtl/GrMtlBuffer.mm FILE: ../../../third_party/skia/src/gpu/mtl/GrMtlCppUtil.h @@ -3472,8 +3474,6 @@ FILE: ../../../third_party/skia/src/gpu/mtl/GrMtlResourceProvider.h FILE: ../../../third_party/skia/src/gpu/mtl/GrMtlResourceProvider.mm FILE: ../../../third_party/skia/src/gpu/mtl/GrMtlSampler.h FILE: ../../../third_party/skia/src/gpu/mtl/GrMtlSampler.mm -FILE: ../../../third_party/skia/src/gpu/mtl/GrMtlStencilAttachment.h -FILE: ../../../third_party/skia/src/gpu/mtl/GrMtlStencilAttachment.mm FILE: ../../../third_party/skia/src/gpu/mtl/GrMtlTextureRenderTarget.h FILE: ../../../third_party/skia/src/gpu/mtl/GrMtlTextureRenderTarget.mm FILE: ../../../third_party/skia/src/gpu/mtl/GrMtlUniformHandler.h @@ -3577,14 +3577,6 @@ FILE: ../../../third_party/skia/bin/sysopen FILE: ../../../third_party/skia/dm/DMGpuTestProcs.cpp FILE: ../../../third_party/skia/example/HelloWorld.cpp FILE: ../../../third_party/skia/example/HelloWorld.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGGradient.cpp -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGGradient.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGPattern.cpp -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGPattern.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGRadialGradient.cpp -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGRadialGradient.h -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGUse.cpp -FILE: ../../../third_party/skia/experimental/svg/model/SkSVGUse.h FILE: ../../../third_party/skia/fuzz/FuzzCanvas.cpp FILE: ../../../third_party/skia/gm/alpha_image.cpp FILE: ../../../third_party/skia/gm/bitmaptiled.cpp @@ -3673,6 +3665,14 @@ FILE: ../../../third_party/skia/modules/sksg/src/SkSGPath.cpp FILE: ../../../third_party/skia/modules/sksg/src/SkSGRect.cpp FILE: ../../../third_party/skia/modules/sksg/src/SkSGRenderNode.cpp FILE: ../../../third_party/skia/modules/sksg/src/SkSGTransform.cpp +FILE: ../../../third_party/skia/modules/svg/include/SkSVGGradient.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGPattern.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGRadialGradient.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGUse.h +FILE: ../../../third_party/skia/modules/svg/src/SkSVGGradient.cpp +FILE: ../../../third_party/skia/modules/svg/src/SkSVGPattern.cpp +FILE: ../../../third_party/skia/modules/svg/src/SkSVGRadialGradient.cpp +FILE: ../../../third_party/skia/modules/svg/src/SkSVGUse.cpp FILE: ../../../third_party/skia/samplecode/SampleCCPRGeometry.cpp FILE: ../../../third_party/skia/samplecode/SampleChineseFling.cpp FILE: ../../../third_party/skia/samplecode/SampleCowboy.cpp @@ -3775,12 +3775,12 @@ FILE: ../../../third_party/skia/src/gpu/gl/GrGLSemaphore.cpp FILE: ../../../third_party/skia/src/gpu/gl/GrGLSemaphore.h FILE: ../../../third_party/skia/src/gpu/glsl/GrGLSLVertexGeoBuilder.cpp FILE: ../../../third_party/skia/src/gpu/glsl/GrGLSLVertexGeoBuilder.h +FILE: ../../../third_party/skia/src/gpu/mock/GrMockAttachment.h FILE: ../../../third_party/skia/src/gpu/mock/GrMockBuffer.h FILE: ../../../third_party/skia/src/gpu/mock/GrMockCaps.h FILE: ../../../third_party/skia/src/gpu/mock/GrMockGpu.cpp FILE: ../../../third_party/skia/src/gpu/mock/GrMockGpu.h FILE: ../../../third_party/skia/src/gpu/mock/GrMockOpsRenderPass.h -FILE: ../../../third_party/skia/src/gpu/mock/GrMockStencilAttachment.h FILE: ../../../third_party/skia/src/gpu/mock/GrMockTexture.h FILE: ../../../third_party/skia/src/gpu/mtl/GrMtlCaps.h FILE: ../../../third_party/skia/src/gpu/mtl/GrMtlCaps.mm @@ -3901,6 +3901,7 @@ FILE: ../../../third_party/skia/gm/ycbcrimage.cpp FILE: ../../../third_party/skia/include/core/SkYUVAInfo.h FILE: ../../../third_party/skia/include/core/SkYUVAPixmaps.h FILE: ../../../third_party/skia/include/gpu/GrBackendSurfaceMutableState.h +FILE: ../../../third_party/skia/include/gpu/GrYUVABackendTextures.h FILE: ../../../third_party/skia/include/gpu/d3d/GrD3DBackendContext.h FILE: ../../../third_party/skia/include/gpu/d3d/GrD3DTypes.h FILE: ../../../third_party/skia/include/gpu/d3d/GrD3DTypesMinimal.h @@ -3909,6 +3910,7 @@ FILE: ../../../third_party/skia/include/private/GrD3DTypesPriv.h FILE: ../../../third_party/skia/include/private/SkIDChangeListener.h FILE: ../../../third_party/skia/include/private/SkSLSampleUsage.h FILE: ../../../third_party/skia/include/utils/SkCustomTypeface.h +FILE: ../../../third_party/skia/modules/canvaskit/gm_bindings.cpp FILE: ../../../third_party/skia/modules/canvaskit/viewer_bindings.cpp FILE: ../../../third_party/skia/modules/canvaskit/wasm_tools/SIMD/simd_float_capabilities.cpp FILE: ../../../third_party/skia/modules/canvaskit/wasm_tools/SIMD/simd_int_capabilities.cpp @@ -3938,8 +3940,11 @@ FILE: ../../../third_party/skia/src/gpu/GrUniformDataManager.cpp FILE: ../../../third_party/skia/src/gpu/GrUniformDataManager.h FILE: ../../../third_party/skia/src/gpu/GrUnrefDDLTask.h FILE: ../../../third_party/skia/src/gpu/GrUtil.cpp +FILE: ../../../third_party/skia/src/gpu/GrYUVABackendTextures.cpp FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DAMDMemoryAllocator.cpp FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DAMDMemoryAllocator.h +FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DAttachment.cpp +FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DAttachment.h FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DBuffer.cpp FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DBuffer.h FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DCaps.cpp @@ -3972,8 +3977,6 @@ FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DRootSignature.cpp FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DRootSignature.h FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DSemaphore.cpp FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DSemaphore.h -FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DStencilAttachment.cpp -FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DStencilAttachment.h FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DTexture.cpp FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DTexture.h FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DTextureRenderTarget.cpp @@ -4004,10 +4007,11 @@ FILE: ../../../third_party/skia/src/sksl/SkSLDehydrator.cpp FILE: ../../../third_party/skia/src/sksl/SkSLDehydrator.h FILE: ../../../third_party/skia/src/sksl/SkSLInliner.cpp FILE: ../../../third_party/skia/src/sksl/SkSLInliner.h +FILE: ../../../third_party/skia/src/sksl/SkSLPool.cpp +FILE: ../../../third_party/skia/src/sksl/SkSLPool.h FILE: ../../../third_party/skia/src/sksl/SkSLSPIRVtoHLSL.cpp FILE: ../../../third_party/skia/src/sksl/SkSLSPIRVtoHLSL.h FILE: ../../../third_party/skia/src/sksl/SkSLSampleUsage.cpp -FILE: ../../../third_party/skia/src/sksl/SkSLTinyUnorderedMap.h FILE: ../../../third_party/skia/src/sksl/ir/SkSLInlineMarker.h FILE: ../../../third_party/skia/src/sksl/ir/SkSLSymbolAlias.h FILE: ../../../third_party/skia/src/utils/SkCustomTypeface.cpp @@ -4081,6 +4085,7 @@ FILE: ../../../third_party/skia/include/core/SkM44.h FILE: ../../../third_party/skia/include/effects/SkStrokeAndFillPathEffect.h FILE: ../../../third_party/skia/include/gpu/GrDirectContext.h FILE: ../../../third_party/skia/include/private/SkOpts_spi.h +FILE: ../../../third_party/skia/include/private/SkTPin.h FILE: ../../../third_party/skia/modules/audioplayer/SkAudioPlayer.cpp FILE: ../../../third_party/skia/modules/audioplayer/SkAudioPlayer.h FILE: ../../../third_party/skia/modules/audioplayer/SkAudioPlayer_mac.mm @@ -4127,6 +4132,7 @@ FILE: ../../../third_party/skia/modules/sksg/src/SkSGGeometryEffect.cpp FILE: ../../../third_party/skia/modules/skshaper/src/SkShaper_coretext.cpp FILE: ../../../third_party/skia/modules/skshaper/src/SkUnicode.h FILE: ../../../third_party/skia/modules/skshaper/src/SkUnicode_icu.cpp +FILE: ../../../third_party/skia/modules/svg/utils/SvgTool.cpp FILE: ../../../third_party/skia/samplecode/Sample3D.cpp FILE: ../../../third_party/skia/samplecode/SampleAudio.cpp FILE: ../../../third_party/skia/samplecode/SampleFitCubicToCircle.cpp @@ -4146,8 +4152,8 @@ FILE: ../../../third_party/skia/src/gpu/GrEagerVertexAllocator.h FILE: ../../../third_party/skia/src/gpu/GrHashMapWithCache.h FILE: ../../../third_party/skia/src/gpu/GrRecordingContextPriv.cpp FILE: ../../../third_party/skia/src/gpu/GrSTArenaList.h -FILE: ../../../third_party/skia/src/gpu/GrThreadSafeUniquelyKeyedProxyViewCache.cpp -FILE: ../../../third_party/skia/src/gpu/GrThreadSafeUniquelyKeyedProxyViewCache.h +FILE: ../../../third_party/skia/src/gpu/GrThreadSafeCache.cpp +FILE: ../../../third_party/skia/src/gpu/GrThreadSafeCache.h FILE: ../../../third_party/skia/src/gpu/ccpr/GrAutoMapVertexBuffer.h FILE: ../../../third_party/skia/src/gpu/effects/GrArithmeticProcessor.fp FILE: ../../../third_party/skia/src/gpu/effects/GrDitherEffect.fp @@ -5591,10 +5597,12 @@ FILE: ../../../third_party/skia/src/gpu/tessellate/GrStrokeTessellateShader.cpp FILE: ../../../third_party/skia/src/gpu/tessellate/GrStrokeTessellateShader.h FILE: ../../../third_party/skia/src/opts/SkVM_opts.h FILE: ../../../third_party/skia/src/sksl/SkSLAnalysis.cpp +FILE: ../../../third_party/skia/src/sksl/SkSLModifiersPool.h FILE: ../../../third_party/skia/src/sksl/SkSLRehydrator.cpp FILE: ../../../third_party/skia/src/sksl/SkSLRehydrator.h FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructor.cpp FILE: ../../../third_party/skia/src/sksl/ir/SkSLIRNode.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLNodeArrayWrapper.h ---------------------------------------------------------------------------------------------------- Copyright 2020 Google LLC. @@ -6337,7 +6345,6 @@ ORIGIN: ../../../third_party/skia/include/gpu/GrConfig.h + ../../../third_party/ TYPE: LicenseType.bsd FILE: ../../../third_party/skia/include/gpu/GrConfig.h FILE: ../../../third_party/skia/include/gpu/GrTypes.h -FILE: ../../../third_party/skia/include/private/GrContext.h FILE: ../../../third_party/skia/src/core/SkImageInfo.cpp FILE: ../../../third_party/skia/src/core/SkRasterClip.cpp FILE: ../../../third_party/skia/src/core/SkRasterClip.h @@ -6477,6 +6484,7 @@ FILE: ../../../third_party/skia/infra/bots/gen_tasks_logic/task_builder.go FILE: ../../../third_party/skia/infra/bots/task_drivers/canary/canary.go FILE: ../../../third_party/skia/infra/bots/task_drivers/cifuzz/cifuzz.go FILE: ../../../third_party/skia/infra/bots/task_drivers/cifuzz/cifuzz_test.go +FILE: ../../../third_party/skia/infra/bots/task_drivers/compile_wasm_gm_tests/compile_wasm_gm_tests.go FILE: ../../../third_party/skia/infra/bots/task_drivers/fm_driver/fm_driver.go FILE: ../../../third_party/skia/infra/bots/task_drivers/g3_canary/g3_canary.go FILE: ../../../third_party/skia/infra/bots/task_drivers/perf_puppeteer_canvas/perf_puppeteer_canvas.go @@ -6486,6 +6494,7 @@ FILE: ../../../third_party/skia/infra/bots/task_drivers/perf_puppeteer_render_sk FILE: ../../../third_party/skia/infra/bots/task_drivers/perf_puppeteer_skottie_frames/perf_puppeteer_skottie_frames.go FILE: ../../../third_party/skia/infra/bots/task_drivers/perf_puppeteer_skottie_frames/perf_puppeteer_skottie_frames_test.go FILE: ../../../third_party/skia/infra/bots/task_drivers/run_gn_to_bp/run_gn_to_bp.go +FILE: ../../../third_party/skia/infra/bots/task_drivers/run_wasm_gm_tests/run_wasm_gm_tests.go ---------------------------------------------------------------------------------------------------- Copyright 2020 The Chromium Authors. All rights reserved. @@ -6601,7 +6610,6 @@ FILE: ../../../third_party/skia/modules/canvaskit/canvaskit/types/canvaskit-wasm FILE: ../../../third_party/skia/modules/canvaskit/canvaskit/types/index.d.ts FILE: ../../../third_party/skia/modules/canvaskit/canvaskit/types/tsconfig.json FILE: ../../../third_party/skia/modules/canvaskit/canvaskit/types/tslint.json -FILE: ../../../third_party/skia/modules/canvaskit/canvaskit/viewer.html FILE: ../../../third_party/skia/modules/pathkit/npm-asmjs/example.html FILE: ../../../third_party/skia/modules/pathkit/npm-asmjs/package.json FILE: ../../../third_party/skia/modules/pathkit/npm-wasm/example.html diff --git a/ci/licenses_golden/licenses_third_party b/ci/licenses_golden/licenses_third_party index b1c841f527adb..d1142db1d2cef 100644 --- a/ci/licenses_golden/licenses_third_party +++ b/ci/licenses_golden/licenses_third_party @@ -1,4 +1,4 @@ -Signature: 5188b32b8a99778d79838f3d3d67036d +Signature: 05cfe341fa933d3a5579dc6246524c80 UNUSED LICENSES: @@ -8025,146 +8025,6 @@ 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. ==================================================================================================== -==================================================================================================== -LIBRARY: dart -LIBRARY: wasmer -ORIGIN: ../../../third_party/dart/benchmarks/Dynamic/dart/Dynamic.dart + ../../../third_party/dart/LICENSE -TYPE: LicenseType.bsd -FILE: ../../../third_party/dart/benchmarks/Dynamic/dart/Dynamic.dart -FILE: ../../../third_party/dart/benchmarks/Dynamic/dart2/Dynamic.dart -FILE: ../../../third_party/dart/benchmarks/EventLoopLatencyJson/dart/EventLoopLatencyJson.dart -FILE: ../../../third_party/dart/benchmarks/EventLoopLatencyJson/dart/json_benchmark.dart -FILE: ../../../third_party/dart/benchmarks/EventLoopLatencyJson/dart/latency.dart -FILE: ../../../third_party/dart/benchmarks/EventLoopLatencyJson/dart2/EventLoopLatencyJson.dart -FILE: ../../../third_party/dart/benchmarks/EventLoopLatencyJson/dart2/json_benchmark.dart -FILE: ../../../third_party/dart/benchmarks/EventLoopLatencyJson/dart2/latency.dart -FILE: ../../../third_party/dart/benchmarks/ListCopy/dart/ListCopy.dart -FILE: ../../../third_party/dart/benchmarks/ListCopy/dart2/ListCopy.dart -FILE: ../../../third_party/dart/benchmarks/MD5/dart/md5.dart -FILE: ../../../third_party/dart/benchmarks/MD5/dart2/md5.dart -FILE: ../../../third_party/dart/benchmarks/RuntimeType/dart/RuntimeType.dart -FILE: ../../../third_party/dart/benchmarks/RuntimeType/dart2/RuntimeType.dart -FILE: ../../../third_party/dart/benchmarks/SHA1/dart/sha1.dart -FILE: ../../../third_party/dart/benchmarks/SHA1/dart2/sha1.dart -FILE: ../../../third_party/dart/benchmarks/SHA256/dart/sha256.dart -FILE: ../../../third_party/dart/benchmarks/SHA256/dart2/sha256.dart -FILE: ../../../third_party/dart/benchmarks/SkeletalAnimation/dart/SkeletalAnimation.dart -FILE: ../../../third_party/dart/benchmarks/SkeletalAnimation/dart2/SkeletalAnimation.dart -FILE: ../../../third_party/dart/benchmarks/SkeletalAnimationSIMD/dart/SkeletalAnimationSIMD.dart -FILE: ../../../third_party/dart/benchmarks/SkeletalAnimationSIMD/dart2/SkeletalAnimationSIMD.dart -FILE: ../../../third_party/dart/benchmarks/TypedDataDuplicate/dart/TypedDataDuplicate.dart -FILE: ../../../third_party/dart/benchmarks/TypedDataDuplicate/dart2/TypedDataDuplicate.dart -FILE: ../../../third_party/dart/benchmarks/Utf8Decode/dart/Utf8Decode.dart -FILE: ../../../third_party/dart/benchmarks/Utf8Decode/dart2/Utf8Decode.dart -FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart/Utf8Encode.dart -FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart2/Utf8Encode.dart -FILE: ../../../third_party/dart/runtime/bin/dartdev_isolate.cc -FILE: ../../../third_party/dart/runtime/bin/dartdev_isolate.h -FILE: ../../../third_party/dart/runtime/bin/exe_utils.cc -FILE: ../../../third_party/dart/runtime/bin/exe_utils.h -FILE: ../../../third_party/dart/runtime/bin/file_win.h -FILE: ../../../third_party/dart/runtime/bin/platform_macos.h -FILE: ../../../third_party/dart/runtime/bin/platform_macos_test.cc -FILE: ../../../third_party/dart/runtime/include/dart_api_dl.c -FILE: ../../../third_party/dart/runtime/include/dart_api_dl.h -FILE: ../../../third_party/dart/runtime/include/dart_version.h -FILE: ../../../third_party/dart/runtime/include/internal/dart_api_dl_impl.h -FILE: ../../../third_party/dart/runtime/observatory/bin/heap_snapshot.dart -FILE: ../../../third_party/dart/runtime/observatory/lib/src/elements/process_snapshot.dart -FILE: ../../../third_party/dart/runtime/platform/leak_sanitizer.h -FILE: ../../../third_party/dart/runtime/platform/unaligned.h -FILE: ../../../third_party/dart/runtime/platform/undefined_behavior_sanitizer.h -FILE: ../../../third_party/dart/runtime/tools/wiki/xref_extractor/bin/main.dart -FILE: ../../../third_party/dart/runtime/tools/wiki/xref_extractor/lib/cquery_driver.dart -FILE: ../../../third_party/dart/runtime/tools/wiki/xref_extractor/lib/xref_extractor.dart -FILE: ../../../third_party/dart/runtime/vm/compiler/aot/dispatch_table_generator.cc -FILE: ../../../third_party/dart/runtime/vm/compiler/aot/dispatch_table_generator.h -FILE: ../../../third_party/dart/runtime/vm/compiler/aot/precompiler_tracer.cc -FILE: ../../../third_party/dart/runtime/vm/compiler/aot/precompiler_tracer.h -FILE: ../../../third_party/dart/runtime/vm/compiler/api/deopt_id.h -FILE: ../../../third_party/dart/runtime/vm/compiler/api/print_filter.cc -FILE: ../../../third_party/dart/runtime/vm/compiler/api/print_filter.h -FILE: ../../../third_party/dart/runtime/vm/compiler/api/type_check_mode.h -FILE: ../../../third_party/dart/runtime/vm/compiler/assembler/assembler_base.h -FILE: ../../../third_party/dart/runtime/vm/compiler/backend/constant_propagator_test.cc -FILE: ../../../third_party/dart/runtime/vm/compiler/backend/reachability_fence_test.cc -FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/abi.cc -FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/abi.h -FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/call.cc -FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/call.h -FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/callback.cc -FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/callback.h -FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/frame_rebase.cc -FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/frame_rebase.h -FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/marshaller.cc -FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/marshaller.h -FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/native_calling_convention.cc -FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/native_calling_convention.h -FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/native_location.cc -FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/native_location.h -FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/native_type.cc -FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/native_type.h -FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/recognized_method.cc -FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/recognized_method.h -FILE: ../../../third_party/dart/runtime/vm/compiler/frontend/kernel_binary_flowgraph_test.cc -FILE: ../../../third_party/dart/runtime/vm/compiler/frontend/multiple_entrypoints_test.cc -FILE: ../../../third_party/dart/runtime/vm/compiler/stub_code_compiler.cc -FILE: ../../../third_party/dart/runtime/vm/compiler/write_barrier_elimination.cc -FILE: ../../../third_party/dart/runtime/vm/compiler/write_barrier_elimination.h -FILE: ../../../third_party/dart/runtime/vm/compiler/write_barrier_elimination_test.cc -FILE: ../../../third_party/dart/runtime/vm/constants_base.h -FILE: ../../../third_party/dart/runtime/vm/dispatch_table.cc -FILE: ../../../third_party/dart/runtime/vm/dispatch_table.h -FILE: ../../../third_party/dart/runtime/vm/field_table.cc -FILE: ../../../third_party/dart/runtime/vm/field_table.h -FILE: ../../../third_party/dart/runtime/vm/port_set.h -FILE: ../../../third_party/dart/runtime/vm/tagged_pointer.h -FILE: ../../../third_party/dart/runtime/vm/timeline_macos.cc -FILE: ../../../third_party/dart/runtime/vm/type_testing_stubs_test.cc -FILE: ../../../third_party/dart/runtime/vm/type_testing_stubs_test.h -FILE: ../../../third_party/dart/runtime/vm/type_testing_stubs_test_arm.cc -FILE: ../../../third_party/dart/runtime/vm/type_testing_stubs_test_arm64.cc -FILE: ../../../third_party/dart/runtime/vm/type_testing_stubs_test_x64.cc -FILE: ../../../third_party/dart/runtime/vm/visitor.cc -FILE: ../../../third_party/dart/samples/ffi/async/async_test.dart -FILE: ../../../third_party/dart/samples/ffi/async/sample_async_callback.dart -FILE: ../../../third_party/dart/samples/ffi/async/sample_native_port_call.dart -FILE: ../../../third_party/dart/samples/ffi/sample_ffi_functions_callbacks_closures.dart -FILE: ../../../third_party/dart/sdk/lib/_internal/js_dev_runtime/patch/js_patch.dart -FILE: ../../../third_party/dart/sdk/lib/_internal/js_runtime/lib/js_patch.dart -FILE: ../../../third_party/dart/sdk/lib/_internal/vm/lib/ffi_struct_patch.dart -FILE: ../../../third_party/dart/sdk/lib/internal/lowering.dart -FILE: ../../../third_party/dart/sdk/lib/io/network_policy.dart -FILE: ../../../third_party/dart/third_party/wasmer/wasmer_wrapper.cc ----------------------------------------------------------------------------------------------------- -Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file -for details. 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 Google Inc. 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 -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. -==================================================================================================== - ==================================================================================================== LIBRARY: dart ORIGIN: ../../../third_party/dart/LICENSE @@ -8249,6 +8109,14 @@ FILE: ../../../third_party/dart/runtime/observatory/web/favicon.ico FILE: ../../../third_party/dart/runtime/observatory/web/index.html FILE: ../../../third_party/dart/runtime/observatory/web/third_party/trace_viewer_full.html FILE: ../../../third_party/dart/runtime/observatory/web/timeline.html +FILE: ../../../third_party/dart/runtime/observatory_2/lib/elements.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/img/chromium_icon.png +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/img/dart_icon.png +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/img/isolate_icon.png +FILE: ../../../third_party/dart/runtime/observatory_2/web/favicon.ico +FILE: ../../../third_party/dart/runtime/observatory_2/web/index.html +FILE: ../../../third_party/dart/runtime/observatory_2/web/third_party/trace_viewer_full.html +FILE: ../../../third_party/dart/runtime/observatory_2/web/timeline.html FILE: ../../../third_party/dart/runtime/tools/wiki/styles/style.scss FILE: ../../../third_party/dart/runtime/tools/wiki/templates/includes/auto-refresh.html FILE: ../../../third_party/dart/runtime/tools/wiki/templates/includes/favicon.html @@ -8362,6 +8230,11 @@ FILE: ../../../third_party/dart/runtime/observatory/lib/src/models/objects/isola FILE: ../../../third_party/dart/runtime/observatory/lib/src/models/repositories/isolate_group.dart FILE: ../../../third_party/dart/runtime/observatory/lib/src/repositories/isolate_group.dart FILE: ../../../third_party/dart/runtime/observatory/lib/src/repositories/timeline_base.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/tree_map.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/isolate_group.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/isolate_group.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/isolate_group.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/timeline_base.dart FILE: ../../../third_party/dart/runtime/platform/elf.h FILE: ../../../third_party/dart/runtime/platform/thread_sanitizer.h FILE: ../../../third_party/dart/runtime/tools/dartfuzz/dartfuzz_api_table.dart @@ -8521,6 +8394,147 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ==================================================================================================== +==================================================================================================== +LIBRARY: dart +ORIGIN: ../../../third_party/dart/benchmarks/Dynamic/dart/Dynamic.dart + ../../../third_party/dart/LICENSE +TYPE: LicenseType.bsd +FILE: ../../../third_party/dart/benchmarks/Dynamic/dart/Dynamic.dart +FILE: ../../../third_party/dart/benchmarks/Dynamic/dart2/Dynamic.dart +FILE: ../../../third_party/dart/benchmarks/EventLoopLatencyJson/dart/EventLoopLatencyJson.dart +FILE: ../../../third_party/dart/benchmarks/EventLoopLatencyJson/dart/json_benchmark.dart +FILE: ../../../third_party/dart/benchmarks/EventLoopLatencyJson/dart/latency.dart +FILE: ../../../third_party/dart/benchmarks/EventLoopLatencyJson/dart2/EventLoopLatencyJson.dart +FILE: ../../../third_party/dart/benchmarks/EventLoopLatencyJson/dart2/json_benchmark.dart +FILE: ../../../third_party/dart/benchmarks/EventLoopLatencyJson/dart2/latency.dart +FILE: ../../../third_party/dart/benchmarks/ListCopy/dart/ListCopy.dart +FILE: ../../../third_party/dart/benchmarks/ListCopy/dart2/ListCopy.dart +FILE: ../../../third_party/dart/benchmarks/MD5/dart/md5.dart +FILE: ../../../third_party/dart/benchmarks/MD5/dart2/md5.dart +FILE: ../../../third_party/dart/benchmarks/RuntimeType/dart/RuntimeType.dart +FILE: ../../../third_party/dart/benchmarks/RuntimeType/dart2/RuntimeType.dart +FILE: ../../../third_party/dart/benchmarks/SHA1/dart/sha1.dart +FILE: ../../../third_party/dart/benchmarks/SHA1/dart2/sha1.dart +FILE: ../../../third_party/dart/benchmarks/SHA256/dart/sha256.dart +FILE: ../../../third_party/dart/benchmarks/SHA256/dart2/sha256.dart +FILE: ../../../third_party/dart/benchmarks/SkeletalAnimation/dart/SkeletalAnimation.dart +FILE: ../../../third_party/dart/benchmarks/SkeletalAnimation/dart2/SkeletalAnimation.dart +FILE: ../../../third_party/dart/benchmarks/SkeletalAnimationSIMD/dart/SkeletalAnimationSIMD.dart +FILE: ../../../third_party/dart/benchmarks/SkeletalAnimationSIMD/dart2/SkeletalAnimationSIMD.dart +FILE: ../../../third_party/dart/benchmarks/TypedDataDuplicate/dart/TypedDataDuplicate.dart +FILE: ../../../third_party/dart/benchmarks/TypedDataDuplicate/dart2/TypedDataDuplicate.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Decode/dart/Utf8Decode.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Decode/dart2/Utf8Decode.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart/Utf8Encode.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Encode/dart2/Utf8Encode.dart +FILE: ../../../third_party/dart/runtime/bin/dartdev_isolate.cc +FILE: ../../../third_party/dart/runtime/bin/dartdev_isolate.h +FILE: ../../../third_party/dart/runtime/bin/exe_utils.cc +FILE: ../../../third_party/dart/runtime/bin/exe_utils.h +FILE: ../../../third_party/dart/runtime/bin/file_win.h +FILE: ../../../third_party/dart/runtime/bin/platform_macos.h +FILE: ../../../third_party/dart/runtime/bin/platform_macos_test.cc +FILE: ../../../third_party/dart/runtime/include/dart_api_dl.c +FILE: ../../../third_party/dart/runtime/include/dart_api_dl.h +FILE: ../../../third_party/dart/runtime/include/dart_version.h +FILE: ../../../third_party/dart/runtime/include/internal/dart_api_dl_impl.h +FILE: ../../../third_party/dart/runtime/observatory/bin/heap_snapshot.dart +FILE: ../../../third_party/dart/runtime/observatory/lib/src/elements/process_snapshot.dart +FILE: ../../../third_party/dart/runtime/observatory_2/bin/heap_snapshot.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/process_snapshot.dart +FILE: ../../../third_party/dart/runtime/platform/leak_sanitizer.h +FILE: ../../../third_party/dart/runtime/platform/unaligned.h +FILE: ../../../third_party/dart/runtime/platform/undefined_behavior_sanitizer.h +FILE: ../../../third_party/dart/runtime/tools/wiki/xref_extractor/bin/main.dart +FILE: ../../../third_party/dart/runtime/tools/wiki/xref_extractor/lib/cquery_driver.dart +FILE: ../../../third_party/dart/runtime/tools/wiki/xref_extractor/lib/xref_extractor.dart +FILE: ../../../third_party/dart/runtime/vm/compiler/aot/dispatch_table_generator.cc +FILE: ../../../third_party/dart/runtime/vm/compiler/aot/dispatch_table_generator.h +FILE: ../../../third_party/dart/runtime/vm/compiler/aot/precompiler_tracer.cc +FILE: ../../../third_party/dart/runtime/vm/compiler/aot/precompiler_tracer.h +FILE: ../../../third_party/dart/runtime/vm/compiler/api/deopt_id.h +FILE: ../../../third_party/dart/runtime/vm/compiler/api/print_filter.cc +FILE: ../../../third_party/dart/runtime/vm/compiler/api/print_filter.h +FILE: ../../../third_party/dart/runtime/vm/compiler/api/type_check_mode.h +FILE: ../../../third_party/dart/runtime/vm/compiler/assembler/assembler_base.h +FILE: ../../../third_party/dart/runtime/vm/compiler/backend/constant_propagator_test.cc +FILE: ../../../third_party/dart/runtime/vm/compiler/backend/reachability_fence_test.cc +FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/abi.cc +FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/abi.h +FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/call.cc +FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/call.h +FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/callback.cc +FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/callback.h +FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/frame_rebase.cc +FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/frame_rebase.h +FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/marshaller.cc +FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/marshaller.h +FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/native_calling_convention.cc +FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/native_calling_convention.h +FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/native_location.cc +FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/native_location.h +FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/native_type.cc +FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/native_type.h +FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/recognized_method.cc +FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/recognized_method.h +FILE: ../../../third_party/dart/runtime/vm/compiler/frontend/kernel_binary_flowgraph_test.cc +FILE: ../../../third_party/dart/runtime/vm/compiler/frontend/multiple_entrypoints_test.cc +FILE: ../../../third_party/dart/runtime/vm/compiler/stub_code_compiler.cc +FILE: ../../../third_party/dart/runtime/vm/compiler/write_barrier_elimination.cc +FILE: ../../../third_party/dart/runtime/vm/compiler/write_barrier_elimination.h +FILE: ../../../third_party/dart/runtime/vm/compiler/write_barrier_elimination_test.cc +FILE: ../../../third_party/dart/runtime/vm/constants_base.h +FILE: ../../../third_party/dart/runtime/vm/datastream_test.cc +FILE: ../../../third_party/dart/runtime/vm/dispatch_table.cc +FILE: ../../../third_party/dart/runtime/vm/dispatch_table.h +FILE: ../../../third_party/dart/runtime/vm/field_table.cc +FILE: ../../../third_party/dart/runtime/vm/field_table.h +FILE: ../../../third_party/dart/runtime/vm/port_set.h +FILE: ../../../third_party/dart/runtime/vm/tagged_pointer.h +FILE: ../../../third_party/dart/runtime/vm/timeline_macos.cc +FILE: ../../../third_party/dart/runtime/vm/type_testing_stubs_test.cc +FILE: ../../../third_party/dart/runtime/vm/type_testing_stubs_test.h +FILE: ../../../third_party/dart/runtime/vm/type_testing_stubs_test_arm.cc +FILE: ../../../third_party/dart/runtime/vm/type_testing_stubs_test_arm64.cc +FILE: ../../../third_party/dart/runtime/vm/type_testing_stubs_test_x64.cc +FILE: ../../../third_party/dart/runtime/vm/visitor.cc +FILE: ../../../third_party/dart/samples/ffi/async/async_test.dart +FILE: ../../../third_party/dart/samples/ffi/async/sample_async_callback.dart +FILE: ../../../third_party/dart/samples/ffi/async/sample_native_port_call.dart +FILE: ../../../third_party/dart/samples/ffi/sample_ffi_functions_callbacks_closures.dart +FILE: ../../../third_party/dart/sdk/lib/_internal/js_dev_runtime/patch/js_patch.dart +FILE: ../../../third_party/dart/sdk/lib/_internal/js_runtime/lib/js_patch.dart +FILE: ../../../third_party/dart/sdk/lib/_internal/vm/lib/ffi_struct_patch.dart +FILE: ../../../third_party/dart/sdk/lib/internal/lowering.dart +FILE: ../../../third_party/dart/sdk/lib/io/network_policy.dart +---------------------------------------------------------------------------------------------------- +Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +for details. 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 Google Inc. 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 +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. +==================================================================================================== + ==================================================================================================== LIBRARY: dart ORIGIN: ../../../third_party/dart/client/dart.js + ../../../third_party/dart/LICENSE @@ -9019,6 +9033,26 @@ FILE: ../../../third_party/dart/runtime/observatory/lib/src/elements/timeline_pa FILE: ../../../third_party/dart/runtime/observatory/lib/src/elements/view_footer.dart FILE: ../../../third_party/dart/runtime/observatory/lib/src/sample_profile/sample_profile.dart FILE: ../../../third_party/dart/runtime/observatory/web/timeline.js +FILE: ../../../third_party/dart/runtime/observatory_2/lib/allocation_profile.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/cli.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/debugger.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/sample_profile.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/allocation_profile/allocation_profile.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/app/analytics.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/cli/command.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/debugger/debugger.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/debugger/debugger_location.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/heap_snapshot.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/logging.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/logging_list.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/megamorphiccache_view.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/objectpool_view.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/persistent_handles.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/ports.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/timeline_page.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/view_footer.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/sample_profile/sample_profile.dart +FILE: ../../../third_party/dart/runtime/observatory_2/web/timeline.js FILE: ../../../third_party/dart/runtime/vm/atomic_test.cc FILE: ../../../third_party/dart/runtime/vm/compiler/aot/precompiler.cc FILE: ../../../third_party/dart/runtime/vm/compiler/aot/precompiler.h @@ -9179,6 +9213,30 @@ FILE: ../../../third_party/dart/runtime/observatory/lib/src/repositories/subtype FILE: ../../../third_party/dart/runtime/observatory/lib/src/repositories/timeline.dart FILE: ../../../third_party/dart/runtime/observatory/lib/src/repositories/unlinked_call.dart FILE: ../../../third_party/dart/runtime/observatory/lib/src/repositories/vm.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/containers/search_bar.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/nav/reload.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/singletargetcache_ref.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/singletargetcache_view.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/subtypetestcache_ref.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/subtypetestcache_view.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/timeline/dashboard.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/unlinkedcall_ref.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/unlinkedcall_view.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/service.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/single_target_cache.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/subtype_test_cache.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/timeline.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/unlinked_call.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/single_target_cache.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/subtype_test_cache.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/timeline.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/unlinked_call.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/vm.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/single_target_cache.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/subtype_test_cache.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/timeline.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/unlinked_call.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/vm.dart FILE: ../../../third_party/dart/runtime/platform/allocation.h FILE: ../../../third_party/dart/runtime/platform/growable_array.h FILE: ../../../third_party/dart/runtime/vm/compilation_trace.cc @@ -9580,6 +9638,153 @@ FILE: ../../../third_party/dart/runtime/observatory/lib/src/repositories/strongl FILE: ../../../third_party/dart/runtime/observatory/lib/src/repositories/target.dart FILE: ../../../third_party/dart/runtime/observatory/lib/src/repositories/type_arguments.dart FILE: ../../../third_party/dart/runtime/observatory/web/timeline_message_handler.js +FILE: ../../../third_party/dart/runtime/observatory_2/lib/event.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/models.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/repositories.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/app/notification.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/class_allocation_profile.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/containers/virtual_collection.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/containers/virtual_tree.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/error_ref.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/general_error.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/helpers/custom_element.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/helpers/nav_bar.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/helpers/nav_menu.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/helpers/rendering_queue.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/helpers/rendering_scheduler.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/helpers/tag.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/helpers/uris.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/inbound_references.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/isolate/counter_chart.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/isolate/location.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/isolate/run_state.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/isolate/shared_summary.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/isolate/summary.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/metric/details.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/metric/graph.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/nav/class_menu.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/nav/isolate_menu.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/nav/library_menu.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/nav/menu_item.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/nav/notify.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/nav/notify_event.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/nav/notify_exception.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/nav/refresh.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/nav/top_menu.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/nav/vm_menu.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/retaining_path.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/source_link.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/strongly_reachable_instances.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/vm_connect_target.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/exceptions.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/allocation_profile.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/breakpoint.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/class.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/code.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/context.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/error.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/event.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/extension_data.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/field.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/flag.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/frame.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/function.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/guarded.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/heap_space.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/icdata.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/inbound_references.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/instance.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/isolate.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/library.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/local_var_descriptors.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/map_association.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/megamorphiccache.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/metric.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/notification.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/object.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/objectpool.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/objectstore.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/pc_descriptors.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/persistent_handles.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/ports.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/retaining_path.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/sample_profile.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/script.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/sentinel.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/source_location.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/target.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/thread.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/timeline_event.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/type_arguments.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/unknown.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/vm.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/objects/zone.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/allocation_profile.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/breakpoint.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/class.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/context.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/editor.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/eval.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/event.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/field.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/flag.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/function.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/heap_snapshot.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/icdata.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/inbound_references.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/instance.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/isolate.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/library.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/megamorphiccache.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/metric.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/notification.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/object.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/objectpool.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/objectstore.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/persistent_handles.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/ports.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/reachable_size.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/retained_size.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/retaining_path.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/sample_profile.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/script.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/strongly_reachable_instances.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/target.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/models/repositories/type_arguments.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/allocation_profile.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/breakpoint.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/class.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/context.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/editor.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/eval.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/event.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/field.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/flag.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/function.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/heap_snapshot.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/icdata.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/inbound_references.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/instance.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/isolate.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/library.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/megamorphiccache.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/metric.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/notification.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/object.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/objectpool.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/objectstore.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/persistent_handles.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/ports.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/reachable_size.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/retained_size.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/retaining_path.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/sample_profile.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/script.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/settings.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/strongly_reachable_instances.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/target.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/repositories/type_arguments.dart +FILE: ../../../third_party/dart/runtime/observatory_2/web/timeline_message_handler.js FILE: ../../../third_party/dart/runtime/platform/syslog_fuchsia.cc FILE: ../../../third_party/dart/runtime/platform/utils_fuchsia.cc FILE: ../../../third_party/dart/runtime/platform/utils_fuchsia.h @@ -9754,6 +9959,58 @@ FILE: ../../../third_party/dart/runtime/observatory/lib/src/elements/type_argume FILE: ../../../third_party/dart/runtime/observatory/lib/src/elements/unknown_ref.dart FILE: ../../../third_party/dart/runtime/observatory/lib/src/elements/vm_view.dart FILE: ../../../third_party/dart/runtime/observatory/lib/tracer.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/app/application.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/app/location_manager.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/class_instances.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/class_ref.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/class_view.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/code_ref.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/code_view.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/context_ref.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/context_view.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/cpu_profile.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/cpu_profile/virtual_tree.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/cpu_profile_table.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/curly_block.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/error_view.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/eval_box.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/field_ref.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/field_view.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/flag_list.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/function_ref.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/function_view.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/heap_snapshot.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/helpers/any_ref.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/icdata_ref.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/icdata_view.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/instance_ref.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/instance_view.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/isolate_ref.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/isolate_view.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/json_view.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/library_ref.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/library_view.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/local_var_descriptors_ref.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/megamorphiccache_ref.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/native_memory_profiler.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/object_common.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/object_view.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/objectpool_ref.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/objectstore_view.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/observatory_application.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/pc_descriptors_ref.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/sample_buffer_control.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/script_inset.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/script_ref.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/script_view.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/sentinel_value.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/sentinel_view.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/source_inset.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/stack_trace_tree_config.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/type_arguments_ref.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/unknown_ref.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/vm_view.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/tracer.dart FILE: ../../../third_party/dart/runtime/platform/atomic.h FILE: ../../../third_party/dart/runtime/platform/signal_blocker.h FILE: ../../../third_party/dart/runtime/vm/allocation.h @@ -9947,7 +10204,6 @@ FILE: ../../../third_party/dart/sdk/lib/io/socket.dart FILE: ../../../third_party/dart/sdk/lib/io/stdio.dart FILE: ../../../third_party/dart/sdk/lib/io/string_transformer.dart FILE: ../../../third_party/dart/sdk/lib/js/js.dart -FILE: ../../../third_party/dart/sdk/lib/math/jenkins_smi_hash.dart FILE: ../../../third_party/dart/sdk/lib/math/point.dart FILE: ../../../third_party/dart/sdk/lib/math/rectangle.dart FILE: ../../../third_party/dart/sdk/lib/mirrors/mirrors.dart @@ -10138,6 +10394,26 @@ FILE: ../../../third_party/dart/runtime/observatory/lib/src/elements/vm_connect. FILE: ../../../third_party/dart/runtime/observatory/lib/src/service/object.dart FILE: ../../../third_party/dart/runtime/observatory/lib/utils.dart FILE: ../../../third_party/dart/runtime/observatory/web/main.dart +FILE: ../../../third_party/dart/runtime/observatory_2/bin/shell.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/app.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/object_graph.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/service.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/service_common.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/service_html.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/service_io.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/app/page.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/app/settings.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/app/view_model.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/allocation_profile.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/class_tree.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/debugger.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/heap_map.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/isolate_reconnect.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/metrics.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/vm_connect.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/service/object.dart +FILE: ../../../third_party/dart/runtime/observatory_2/lib/utils.dart +FILE: ../../../third_party/dart/runtime/observatory_2/web/main.dart FILE: ../../../third_party/dart/runtime/platform/address_sanitizer.h FILE: ../../../third_party/dart/runtime/platform/memory_sanitizer.h FILE: ../../../third_party/dart/runtime/platform/safe_stack.h @@ -10256,6 +10532,7 @@ LIBRARY: dart ORIGIN: ../../../third_party/dart/runtime/observatory/web/third_party/webcomponents.min.js TYPE: LicenseType.bsd FILE: ../../../third_party/dart/runtime/observatory/web/third_party/webcomponents.min.js +FILE: ../../../third_party/dart/runtime/observatory_2/web/third_party/webcomponents.min.js ---------------------------------------------------------------------------------------------------- Copyright (c) 2014 The Polymer Authors. All rights reserved. @@ -22079,7 +22356,6 @@ LIBRARY: wasmer ORIGIN: ../../../third_party/dart/third_party/wasmer/LICENSE TYPE: LicenseType.mit FILE: ../../../third_party/dart/third_party/wasmer/Cargo.toml -FILE: ../../../third_party/dart/third_party/wasmer/wasmer.hh FILE: ../../../third_party/dart/third_party/wasmer/wasmer.rs ---------------------------------------------------------------------------------------------------- MIT License diff --git a/ci/pubspec.yaml b/ci/pubspec.yaml index eba8dd49bfbbf..b66758dd1ac34 100644 --- a/ci/pubspec.yaml +++ b/ci/pubspec.yaml @@ -8,7 +8,7 @@ dependencies: args: ^1.6.0 path: ^1.7.0 isolate: ^2.0.3 - process_runner: ^3.0.0 + process_runner: ^3.1.1 environment: sdk: '>=2.8.0 <3.0.0' diff --git a/common/config.gni b/common/config.gni index f7b122a3f480e..3173a5e15b069 100644 --- a/common/config.gni +++ b/common/config.gni @@ -28,6 +28,7 @@ feature_defines_list = [ "FLUTTER_RUNTIME_MODE_PROFILE=2", "FLUTTER_RUNTIME_MODE_RELEASE=3", "FLUTTER_RUNTIME_MODE_JIT_RELEASE=4", + "DART_LEGACY_API=[[deprecated]]", ] if (flutter_runtime_mode == "debug") { diff --git a/common/settings.cc b/common/settings.cc index 1508e213bcc6c..ec8ddf60f9c8d 100644 --- a/common/settings.cc +++ b/common/settings.cc @@ -46,6 +46,8 @@ std::string Settings::ToString() const { stream << "enable_dart_profiling: " << enable_dart_profiling << std::endl; stream << "disable_dart_asserts: " << disable_dart_asserts << std::endl; stream << "enable_observatory: " << enable_observatory << std::endl; + stream << "enable_observatory_publication: " << enable_observatory_publication + << std::endl; stream << "observatory_host: " << observatory_host << std::endl; stream << "observatory_port: " << observatory_port << std::endl; stream << "use_test_fonts: " << use_test_fonts << std::endl; diff --git a/common/settings.h b/common/settings.h index 46f88399b407e..c956423ca8914 100644 --- a/common/settings.h +++ b/common/settings.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -50,15 +51,16 @@ using UnhandledExceptionCallback = std::function; -// TODO(chinmaygarde): Deprecate all the "path" struct members in favor of the +// TODO(26783): Deprecate all the "path" struct members in favor of the // callback that generates the mapping from these paths. -// https://github.com/flutter/flutter/issues/26783 using MappingCallback = std::function(void)>; -using MappingsCallback = - std::function>(void)>; +using Mappings = std::vector>; +using MappingsCallback = std::function; using FrameRasterizedCallback = std::function; +class DartIsolate; + struct Settings { Settings(); @@ -126,6 +128,11 @@ struct Settings { // Whether the Dart VM service should be enabled. bool enable_observatory = false; + // Whether to publish the observatory URL over mDNS. + // On iOS 14 this prompts a local network permission dialog, + // which cannot be accepted or dismissed in a CI environment. + bool enable_observatory_publication = true; + // The IP address to which the Dart VM service is bound. std::string observatory_host; @@ -164,12 +171,22 @@ struct Settings { TaskObserverRemove task_observer_remove; // The main isolate is current when this callback is made. This is a good spot // to perform native Dart bindings for libraries not built in. - fml::closure root_isolate_create_callback; + std::function root_isolate_create_callback; + // TODO(68738): Update isolate callbacks in settings to accept an additional + // DartIsolate parameter. fml::closure isolate_create_callback; // The isolate is not current and may have already been destroyed when this // call is made. fml::closure root_isolate_shutdown_callback; fml::closure isolate_shutdown_callback; + // A callback made in the isolate scope of the service isolate when it is + // launched. Care must be taken to ensure that callers are assigning callbacks + // to the settings object used to launch the VM. If an existing VM is used to + // launch an isolate using these settings, the callback will be ignored as the + // service isolate has already been launched. Also, this callback will only be + // made in the modes in which the service isolate is eligible for launch + // (debug and profile). + fml::closure service_isolate_create_callback; // The callback made on the UI thread in an isolate scope when the engine // detects that the framework is idle. The VM also uses this time to perform // tasks suitable when idling. Due to this, embedders are still advised to be diff --git a/e2etests/web/regular_integration_tests/README.md b/e2etests/web/regular_integration_tests/README.md index ba7f0c6c735d2..5e3501564680e 100644 --- a/e2etests/web/regular_integration_tests/README.md +++ b/e2etests/web/regular_integration_tests/README.md @@ -37,3 +37,38 @@ flutter drive -v --target=test_driver/text_editing_integration.dart -d web-serve ``` More details for "Running Flutter Driver tests with Web" can be found in [wiki](https://github.com/flutter/flutter/wiki/Running-Flutter-Driver-tests-with-Web). + +## Adding screenshot tests + +In order to test screenshot tests the tests on the driver side needs to call the `integration_test` package with an `onScreenshot` callback which can do a comparison between the `screenshotBytes` taken during the test and a golden file. We added a utility method that can do this comparison by using a golden in `flutter/goldens` repository. + +In order to use screenshot testing first, import `screenshot_support.dart` from the driver side test (example: `text_editing_integration_test.dart`). Default value for `diffRateFailure` is 0.5. + +``` +import 'package:regular_integration_tests/screenshot_support.dart' as test; + +Future main() async { + final double kMaxDiffRateFailure = 0.1; + await test.runTestWithScreenshots(diffRateFailure = kMaxDiffRateFailure); +} +``` + +In order to run the tests follow these steps: + +1. You can use two different approaches, using [felt](https://github.com/flutter/engine/blob/master/lib/web_ui/dev/README.md) tool will run all the tests, hence update all the goldens. For running individual tests, we need to set UPDATE_GOLDENS environment variable. Screenshots are saved differently per browser, therefore do not forget to also update the screenshots for other browsers. Note that, LUCI is only running screenshot testing for integration tests on Firefox and Chrome. + +``` +felt test --integration-tests-only --update-screenshot-goldens +``` + +``` +UPDATE_GOLDENS=true flutter drive -v --target=test_driver/text_editing_integration.dart -d web-server --release --local-engine=host_debug_unopt +``` + +``` +UPDATE_GOLDENS=true flutter drive -v --target=test_driver/text_editing_integration.dart -d web-server --release --local-engine=host_debug_unopt --browser-name=firefox +``` + +2. The golden will be under `engine/src/flutter/lib/web_ui/.dart_tool/goldens/engine/web/` directory, you should create a PR for that file and merge it to `flutter/goldens`. For each browser the browser name would be appended to the end of the golden file such as: `screenshot_name-chrome.png` or `screenshot_name-firefox.png` + +3. Get the commit SHA and replace the `revision` in this file: `engine/src/flutter/lib/web_ui/dev/goldens_lock.yaml` diff --git a/e2etests/web/regular_integration_tests/fonts/RobotoMono-Bold.ttf b/e2etests/web/regular_integration_tests/fonts/RobotoMono-Bold.ttf new file mode 100644 index 0000000000000..900fce6848210 Binary files /dev/null and b/e2etests/web/regular_integration_tests/fonts/RobotoMono-Bold.ttf differ diff --git a/e2etests/web/regular_integration_tests/fonts/RobotoMono-Regular.ttf b/e2etests/web/regular_integration_tests/fonts/RobotoMono-Regular.ttf new file mode 100644 index 0000000000000..7c4ce36a442d1 Binary files /dev/null and b/e2etests/web/regular_integration_tests/fonts/RobotoMono-Regular.ttf differ diff --git a/e2etests/web/regular_integration_tests/lib/screenshot_support.dart b/e2etests/web/regular_integration_tests/lib/screenshot_support.dart new file mode 100644 index 0000000000000..bfdf34ff0000d --- /dev/null +++ b/e2etests/web/regular_integration_tests/lib/screenshot_support.dart @@ -0,0 +1,91 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' as io; +import 'dart:math'; + +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:integration_test/integration_test_driver_extended.dart' as test; + +import 'package:web_test_utils/goldens.dart'; +import 'package:web_test_utils/image_compare.dart'; +import 'package:webdriver/src/async/window.dart'; + +import 'package:image/image.dart'; + +/// Tolerable pixel difference ratio between the goldens and the screenshots. +/// +/// We are allowing a higher difference rate compared to the unit tests (where +/// this rate is set to 0.28), since during the end to end tests there are +/// more components on the screen which are not related to the functionality +/// under test ex: a blinking cursor. +const double kMaxDiffRateFailure = 0.5 / 100; // 0.5% + +/// SBrowser screen dimensions for the Flutter Driver test. +const int _kScreenshotWidth = 1024; +const int _kScreenshotHeight = 1024; + +/// Used for calling `integration_test` package. +/// +/// Compared to other similar classes which only included the following call: +/// ``` +/// Future main() async => test.integrationDriver(); +/// ``` +/// +/// this method is able to take screenshot. +/// +/// It provides an `onScreenshot` callback to the `integrationDriver` method. +/// It also includes options for updating the golden files. +Future runTestWithScreenshots( + {double diffRateFailure = kMaxDiffRateFailure, + int browserWidth = _kScreenshotWidth, + int browserHeight = _kScreenshotHeight}) async { + final WebFlutterDriver driver = + await FlutterDriver.connect() as WebFlutterDriver; + + // Learn the browser in use from the webDriver. + final String browser = driver.webDriver.capabilities['browserName'] as String; + + final Window window = await driver.webDriver.window; + window.setSize(Rectangle(0, 0, browserWidth, browserHeight)); + + bool updateGoldens = false; + // We are using an environment variable instead of an argument, since + // this code is not invoked from the shell but from the `flutter drive` + // tool itself, we do not have control on the command line arguments. + // Please read the README, further info on how to update the goldens. + final String updateGoldensFlag = io.Platform.environment['UPDATE_GOLDENS']; + // Validate if the environment variable is set correctly. + if (updateGoldensFlag != null && + !(updateGoldensFlag.toLowerCase() == 'true' || + updateGoldensFlag.toLowerCase() == 'false')) { + throw StateError( + 'UPDATE_GOLDENS environment variable is not set correctly'); + } + if (updateGoldensFlag != null && updateGoldensFlag.toLowerCase() == 'true') { + updateGoldens = true; + } + + test.integrationDriver( + driver: driver, + onScreenshot: (String screenshotName, List screenshotBytes) async { + final Image screenshot = decodePng(screenshotBytes); + final String result = compareImage( + screenshot, + updateGoldens, + '$screenshotName-$browser.png', + PixelComparison.fuzzy, + diffRateFailure, + forIntegrationTests: true, + write: updateGoldens, + ); + if (result == 'OK') { + return true; + } else { + io.stderr.writeln('ERROR: $result'); + return false; + } + }, + ); +} diff --git a/e2etests/web/regular_integration_tests/lib/text_editing_main.dart b/e2etests/web/regular_integration_tests/lib/text_editing_main.dart index f1aefa7ef1967..d36c0c7ae9f6c 100644 --- a/e2etests/web/regular_integration_tests/lib/text_editing_main.dart +++ b/e2etests/web/regular_integration_tests/lib/text_editing_main.dart @@ -11,6 +11,7 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( key: const Key('mainapp'), + theme: ThemeData(fontFamily: 'RobotoMono'), title: 'Integration Test App', home: MyHomePage(title: 'Integration Test App'), ); @@ -56,6 +57,7 @@ class _MyHomePageState extends State { enabled: true, controller: _emptyController, decoration: const InputDecoration( + contentPadding: EdgeInsets.all(10.0), labelText: 'Empty Input Field:', ), ), @@ -67,6 +69,7 @@ class _MyHomePageState extends State { enabled: true, controller: _controller, decoration: const InputDecoration( + contentPadding: EdgeInsets.all(10.0), labelText: 'Text Input Field:', ), ), @@ -78,6 +81,7 @@ class _MyHomePageState extends State { enabled: true, controller: _controller2, decoration: const InputDecoration( + contentPadding: EdgeInsets.all(10.0), labelText: 'Text Input Field 2:', ), onFieldSubmitted: (String str) { @@ -94,7 +98,7 @@ class _MyHomePageState extends State { child: SelectableText( 'Lorem ipsum dolor sit amet', key: Key('selectable'), - style: TextStyle(fontFamily: 'Roboto', fontSize: 20.0), + style: TextStyle(fontFamily: 'RobotoMono', fontSize: 20.0), ), ), ], diff --git a/e2etests/web/regular_integration_tests/pubspec.yaml b/e2etests/web/regular_integration_tests/pubspec.yaml index 26ee22f810be2..407980bc02496 100644 --- a/e2etests/web/regular_integration_tests/pubspec.yaml +++ b/e2etests/web/regular_integration_tests/pubspec.yaml @@ -15,8 +15,14 @@ dev_dependencies: sdk: flutter integration_test: 0.9.0 http: 0.12.0+2 - test: any + web_test_utils: + path: ../../../web_sdk/web_test_utils flutter: assets: - assets/images/ + fonts: + - family: RobotoMono + fonts: + - asset: fonts/RobotoMono-Bold.ttf + - asset: fonts/RobotoMono-Regular.ttf diff --git a/e2etests/web/regular_integration_tests/test_driver/text_editing_integration.dart b/e2etests/web/regular_integration_tests/test_driver/text_editing_integration.dart index af1b8d6b00a0f..49247d7d79cfb 100644 --- a/e2etests/web/regular_integration_tests/test_driver/text_editing_integration.dart +++ b/e2etests/web/regular_integration_tests/test_driver/text_editing_integration.dart @@ -13,7 +13,7 @@ import 'package:flutter/material.dart'; import 'package:integration_test/integration_test.dart'; void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final IntegrationTestWidgetsFlutterBinding binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized() as IntegrationTestWidgetsFlutterBinding; testWidgets('Focused text field creates a native input element', (WidgetTester tester) async { @@ -41,6 +41,8 @@ void main() { textFormField.controller.text = 'New Value'; // DOM element's value also changes. expect(input.value, 'New Value'); + + await binding.takeScreenshot('focused_text_field'); }); testWidgets('Input field with no initial value works', @@ -177,6 +179,21 @@ void main() { 'cancelable': true, }); + // Press and release AltGr key. + // Regression test for https://github.com/flutter/flutter/issues/58979. + dispatchKeyboardEvent(input, 'keydown', { + 'key': 'AltGraph', + 'code': 'AltRight', + 'bubbles': true, + 'cancelable': true, + }); + dispatchKeyboardEvent(input, 'keyup', { + 'key': 'AltGraph', + 'code': 'AltRight', + 'bubbles': true, + 'cancelable': true, + }); + // Press Tab. The focus should move to the next TextFormField. dispatchKeyboardEvent(input, 'keydown', { 'key': 'Tab', diff --git a/e2etests/web/regular_integration_tests/test_driver/text_editing_integration_test.dart b/e2etests/web/regular_integration_tests/test_driver/text_editing_integration_test.dart index 96b5ad0bf52a4..9c2c0fdcadc77 100644 --- a/e2etests/web/regular_integration_tests/test_driver/text_editing_integration_test.dart +++ b/e2etests/web/regular_integration_tests/test_driver/text_editing_integration_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:integration_test/integration_test_driver.dart' as test; +import 'package:regular_integration_tests/screenshot_support.dart' as test; -Future main() async => test.integrationDriver(); +Future main() async { + await test.runTestWithScreenshots(); +} diff --git a/flow/layers/child_scene_layer.cc b/flow/layers/child_scene_layer.cc index 9ba5eff3c5021..858328a140b22 100644 --- a/flow/layers/child_scene_layer.cc +++ b/flow/layers/child_scene_layer.cc @@ -22,9 +22,7 @@ void ChildSceneLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { CheckForChildLayerBelow(context); } -void ChildSceneLayer::Paint(PaintContext& context) const { - FML_NOTREACHED(); -} +void ChildSceneLayer::Paint(PaintContext& context) const {} void ChildSceneLayer::UpdateScene(SceneUpdateContext& context) { TRACE_EVENT0("flutter", "ChildSceneLayer::UpdateScene"); diff --git a/flutter_frontend_server/lib/server.dart b/flutter_frontend_server/lib/server.dart index 861ad11c572e5..1ced42a4d0de4 100644 --- a/flutter_frontend_server/lib/server.dart +++ b/flutter_frontend_server/lib/server.dart @@ -147,8 +147,6 @@ Future starter( '--target=flutter', '--track-widget-creation', '--enable-asserts', - '--gen-bytecode', - '--bytecode-options=source-positions,local-var-info,debugger-stops,instance-field-initializers,keep-unreachable-code,avoid-closure-call-instructions', ]); compiler ??= _FlutterFrontendCompiler( output, diff --git a/fml/BUILD.gn b/fml/BUILD.gn index 358fbee5483ff..cf6af4e68a2e2 100644 --- a/fml/BUILD.gn +++ b/fml/BUILD.gn @@ -139,7 +139,7 @@ source_set("fml") { "platform/darwin/string_range_sanitization.mm", ] - libs += [ "Foundation.framework" ] + frameworks = [ "Foundation.framework" ] } if (is_android) { @@ -249,6 +249,7 @@ if (enable_unittests) { "command_line_unittest.cc", "file_unittest.cc", "hash_combine_unittests.cc", + "logging_unittests.cc", "memory/ref_counted_unittest.cc", "memory/task_runner_checker_unittest.cc", "memory/weak_ptr_unittest.cc", diff --git a/fml/dart/dart_converter.h b/fml/dart/dart_converter.h index 159ec44e25c31..ccdd2422abed6 100644 --- a/fml/dart/dart_converter.h +++ b/fml/dart/dart_converter.h @@ -23,7 +23,11 @@ struct DartConverter { return Dart_Null(); } - auto dart_list_handle = Dart_NewListOf(Dart_CoreType_Int, val->GetSize()); + auto dart_list_handle = Dart_NewListOfTypeFilled( + ToDartTypeHandle(), // type + CreateZeroInitializedDartObject(), // sentinel + val->GetSize() // size + ); if (Dart_IsError(dart_list_handle)) { FML_LOG(ERROR) << "Error while attempting to allocate a list: " diff --git a/fml/logging.cc b/fml/logging.cc index d4273b9381da7..6530348e0ccac 100644 --- a/fml/logging.cc +++ b/fml/logging.cc @@ -93,7 +93,7 @@ LogMessage::~LogMessage() { #endif if (severity_ >= LOG_FATAL) { - abort(); + KillProcess(); } } @@ -105,4 +105,8 @@ bool ShouldCreateLogMessage(LogSeverity severity) { return severity >= GetMinLogLevel(); } +void KillProcess() { + abort(); +} + } // namespace fml diff --git a/fml/logging.h b/fml/logging.h index 20cb887cb20df..0b732ee818a0a 100644 --- a/fml/logging.h +++ b/fml/logging.h @@ -43,6 +43,8 @@ int GetVlogVerbosity(); // LOG_FATAL and above is always true. bool ShouldCreateLogMessage(LogSeverity severity); +[[noreturn]] void KillProcess(); + } // namespace fml #define FML_LOG_STREAM(severity) \ @@ -87,9 +89,10 @@ bool ShouldCreateLogMessage(LogSeverity severity); #define FML_DCHECK(condition) FML_EAT_STREAM_PARAMETERS(condition) #endif -#define FML_NOTREACHED() FML_DCHECK(false) - -#define FML_NOTIMPLEMENTED() \ - FML_LOG(ERROR) << "Not implemented in: " << __PRETTY_FUNCTION__ +#define FML_UNREACHABLE() \ + { \ + FML_LOG(ERROR) << "Reached unreachable code."; \ + ::fml::KillProcess(); \ + } #endif // FLUTTER_FML_LOGGING_H_ diff --git a/fml/logging_unittests.cc b/fml/logging_unittests.cc new file mode 100644 index 0000000000000..88b2ebee806df --- /dev/null +++ b/fml/logging_unittests.cc @@ -0,0 +1,32 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/fml/logging.h" +#include "gtest/gtest.h" + +namespace fml { +namespace testing { + +int UnreachableScopeWithoutReturnDoesNotMakeCompilerMad() { + KillProcess(); + // return 0; <--- Missing but compiler is fine. +} + +int UnreachableScopeWithMacroWithoutReturnDoesNotMakeCompilerMad() { + FML_UNREACHABLE(); + // return 0; <--- Missing but compiler is fine. +} + +TEST(LoggingTest, UnreachableKillProcess) { + ::testing::FLAGS_gtest_death_test_style = "threadsafe"; + ASSERT_DEATH(KillProcess(), ""); +} + +TEST(LoggingTest, UnreachableKillProcessWithMacro) { + ::testing::FLAGS_gtest_death_test_style = "threadsafe"; + ASSERT_DEATH({ FML_UNREACHABLE(); }, ""); +} + +} // namespace testing +} // namespace fml diff --git a/lib/io/dart_io.cc b/lib/io/dart_io.cc index 21b5952b15233..a4dfa9351c095 100644 --- a/lib/io/dart_io.cc +++ b/lib/io/dart_io.cc @@ -23,7 +23,7 @@ void DartIO::InitForIsolate(bool may_insecurely_connect_to_all_domains, FML_CHECK(!LogIfError(result)); Dart_Handle embedder_config_type = - Dart_GetType(io_lib, ToDart("_EmbedderConfig"), 0, nullptr); + Dart_GetNonNullableType(io_lib, ToDart("_EmbedderConfig"), 0, nullptr); FML_CHECK(!LogIfError(embedder_config_type)); Dart_Handle allow_insecure_connections_result = Dart_SetField( diff --git a/lib/ui/compositing.dart b/lib/ui/compositing.dart index f484f0e53c521..1bf7b1fd60a38 100644 --- a/lib/ui/compositing.dart +++ b/lib/ui/compositing.dart @@ -37,7 +37,7 @@ class Scene extends NativeFieldWrapperClass2 { ); } - String _toImage(int width, int height, _Callback<_Image> callback) native 'Scene_toImage'; + String? _toImage(int width, int height, _Callback<_Image> callback) native 'Scene_toImage'; /// Releases the resources used by this scene. /// @@ -538,7 +538,7 @@ class SceneBuilder extends NativeFieldWrapperClass2 { return layer; } - EngineLayer _pushShaderMask( + void _pushShaderMask( EngineLayer engineLayer, Shader shader, double maskRectLeft, @@ -587,7 +587,7 @@ class SceneBuilder extends NativeFieldWrapperClass2 { return layer; } - EngineLayer _pushPhysicalShape(EngineLayer outEngineLayer, Path path, double elevation, int color, int shadowColor, + void _pushPhysicalShape(EngineLayer outEngineLayer, Path path, double elevation, int color, int shadowColor, int clipBehavior) native 'SceneBuilder_pushPhysicalShape'; /// Ends the effect of the most recently pushed operation. diff --git a/lib/ui/dart_runtime_hooks.cc b/lib/ui/dart_runtime_hooks.cc index 3512f9b446cb8..cb619aadc1a42 100644 --- a/lib/ui/dart_runtime_hooks.cc +++ b/lib/ui/dart_runtime_hooks.cc @@ -62,17 +62,19 @@ void DartRuntimeHooks::RegisterNatives(tonic::DartLibraryNatives* natives) { static void PropagateIfError(Dart_Handle result) { if (Dart_IsError(result)) { + FML_LOG(ERROR) << "Dart Error: " << ::Dart_GetError(result); Dart_PropagateError(result); } } -static Dart_Handle GetFunction(Dart_Handle builtin_library, const char* name) { +static Dart_Handle InvokeFunction(Dart_Handle builtin_library, + const char* name) { Dart_Handle getter_name = ToDart(name); return Dart_Invoke(builtin_library, getter_name, 0, nullptr); } static void InitDartInternal(Dart_Handle builtin_library, bool is_ui_isolate) { - Dart_Handle print = GetFunction(builtin_library, "_getPrintClosure"); + Dart_Handle print = InvokeFunction(builtin_library, "_getPrintClosure"); Dart_Handle internal_library = Dart_LookupLibrary(ToDart("dart:_internal")); @@ -112,7 +114,7 @@ static void InitDartAsync(Dart_Handle builtin_library, bool is_ui_isolate) { Dart_Handle schedule_microtask; if (is_ui_isolate) { schedule_microtask = - GetFunction(builtin_library, "_getScheduleMicrotaskClosure"); + InvokeFunction(builtin_library, "_getScheduleMicrotaskClosure"); } else { Dart_Handle isolate_lib = Dart_LookupLibrary(ToDart("dart:isolate")); Dart_Handle method_name = @@ -130,21 +132,24 @@ static void InitDartIO(Dart_Handle builtin_library, const std::string& script_uri) { Dart_Handle io_lib = Dart_LookupLibrary(ToDart("dart:io")); Dart_Handle platform_type = - Dart_GetType(io_lib, ToDart("_Platform"), 0, nullptr); + Dart_GetNonNullableType(io_lib, ToDart("_Platform"), 0, nullptr); if (!script_uri.empty()) { Dart_Handle result = Dart_SetField(platform_type, ToDart("_nativeScript"), ToDart(script_uri)); PropagateIfError(result); } - Dart_Handle locale_closure = - GetFunction(builtin_library, "_getLocaleClosure"); + // typedef _LocaleClosure = String Function(); + Dart_Handle /* _LocaleClosure? */ locale_closure = + InvokeFunction(builtin_library, "_getLocaleClosure"); + PropagateIfError(locale_closure); + // static String Function()? _localeClosure; Dart_Handle result = Dart_SetField(platform_type, ToDart("_localeClosure"), locale_closure); PropagateIfError(result); // Register dart:io service extensions used for network profiling. Dart_Handle network_profiling_type = - Dart_GetType(io_lib, ToDart("_NetworkProfiling"), 0, nullptr); + Dart_GetNonNullableType(io_lib, ToDart("_NetworkProfiling"), 0, nullptr); PropagateIfError(network_profiling_type); result = Dart_Invoke(network_profiling_type, ToDart("_registerServiceExtension"), 0, nullptr); diff --git a/lib/ui/hooks.dart b/lib/ui/hooks.dart index a911d79f63a8d..f3e4d1ee5ec81 100644 --- a/lib/ui/hooks.dart +++ b/lib/ui/hooks.dart @@ -53,15 +53,15 @@ void _updateWindowMetrics( _invoke(window.onMetricsChanged, window._onMetricsChangedZone); } -typedef _LocaleClosure = String? Function(); - -String? _localeClosure() { +String _localeClosure() { if (window.locale == null) { - return null; + return ''; } return window.locale.toString(); } +typedef _LocaleClosure = String Function(); + @pragma('vm:entry-point') // ignore: unused_element _LocaleClosure? _getLocaleClosure() => _localeClosure; @@ -210,9 +210,7 @@ void _drawFrame() { } // ignore: always_declare_return_types, prefer_generic_function_type_aliases -typedef _UnaryFunction(Null args); -// ignore: always_declare_return_types, prefer_generic_function_type_aliases -typedef _BinaryFunction(Null args, Null message); +typedef _ListStringArgFunction(List args); @pragma('vm:entry-point') // ignore: unused_element @@ -221,11 +219,7 @@ void _runMainZoned(Function startMainIsolateFunction, List args) { startMainIsolateFunction((){ runZonedGuarded(() { - if (userMainFunction is _BinaryFunction) { - // This seems to be undocumented but supported by the command line VM. - // Let's do the same in case old entry-points are ported to Flutter. - (userMainFunction as dynamic)(args, ''); - } else if (userMainFunction is _UnaryFunction) { + if (userMainFunction is _ListStringArgFunction) { (userMainFunction as dynamic)(args); } else { userMainFunction(); diff --git a/lib/ui/painting.dart b/lib/ui/painting.dart index d6b9ea1295e66..2c777bad5ca5b 100644 --- a/lib/ui/painting.dart +++ b/lib/ui/painting.dart @@ -3504,6 +3504,9 @@ class FragmentShader extends Shader { /// TODO(clocksmith): Public Docs. void setImage(Image image, TileMode tmx, TileMode tmy, Float64List matrix4) native 'FragmentShader_setImage'; + + /// TODO(clocksmith): Public Docs. + void refresh() native 'FragmentShader_refresh'; } /// A shader (as used by [Paint.shader]) that tiles an image. @@ -4719,7 +4722,7 @@ class Picture extends NativeFieldWrapperClass2 { ); } - String _toImage(int width, int height, _Callback<_Image> callback) native 'Picture_toImage'; + String? _toImage(int width, int height, _Callback<_Image> callback) native 'Picture_toImage'; /// Release the resources used by this object. The object is no longer usable /// after this method is called. diff --git a/lib/ui/painting/fragment_shader.cc b/lib/ui/painting/fragment_shader.cc index a53f83d06e024..a4985517d8e7b 100644 --- a/lib/ui/painting/fragment_shader.cc +++ b/lib/ui/painting/fragment_shader.cc @@ -28,7 +28,8 @@ IMPLEMENT_WRAPPERTYPEINFO(ui, FragmentShader); V(FragmentShader, initWithSource) \ V(FragmentShader, initWithSPIRV) \ V(FragmentShader, setTime) \ - V(FragmentShader, setImage) + V(FragmentShader, setImage) \ + V(FragmentShader, refresh) FOR_EACH_BINDING(DART_NATIVE_CALLBACK) @@ -47,6 +48,7 @@ void FragmentShader::initWithSPIRV(const tonic::Uint8List& data) { return; } auto sksl = transpiler->GetSkSL(); + uniformData_ = std::make_unique(Dart_NewTypedData(Dart_TypedData_kFloat32, transpiler->GetUniformBufferSize())); initWithSource(sksl); } @@ -64,11 +66,23 @@ void FragmentShader::setImage(CanvasImage* image, setShader(); } +void FragmentShader::refresh() { + sk_sp uniforms = SkData::MakeWithoutCopy(uniformData_->data(), uniformData_->num_elements() * 4); + set_shader(UIDartState::CreateGPUObject(runtime_effect_->makeShader(uniforms, nullptr, 0, nullptr, false))); + // set_shader(UIDartState::CreateGPUObject(builder_->makeShader(nullptr, false))); +} + +// FragmentShader::uniformData() { +// return _uniformData; +// } + void FragmentShader::initEffect(SkString sksl) { SkString err; std::tie(runtime_effect_, err) = SkRuntimeEffect::Make(sksl); if (!runtime_effect_) { FML_DLOG(ERROR) << "Invalid SKSL:\n" << sksl.c_str() << "\nSKSL Error:\n" << err.c_str(); + } else { + FML_DLOG(ERROR) << "Valid SKSL:\n" << sksl.c_str(); } } diff --git a/lib/ui/painting/fragment_shader.h b/lib/ui/painting/fragment_shader.h index 24439b68c513e..fcc575b141957 100644 --- a/lib/ui/painting/fragment_shader.h +++ b/lib/ui/painting/fragment_shader.h @@ -41,6 +41,10 @@ class FragmentShader : public Shader { SkTileMode tmy, const tonic::Float64List& matrix4); + void refresh(); + + // uniformData(); + static void RegisterNatives(tonic::DartLibraryNatives* natives); private: @@ -53,6 +57,8 @@ class FragmentShader : public Shader { float t_; sk_sp input_; + std::unique_ptr uniformData_; + // Since the sksl cannot be updated, the effect can be // created once and re-used. sk_sp runtime_effect_; diff --git a/lib/ui/painting/image_decoder.cc b/lib/ui/painting/image_decoder.cc index a2de0185bb285..d6c4a668fb5fc 100644 --- a/lib/ui/painting/image_decoder.cc +++ b/lib/ui/painting/image_decoder.cc @@ -105,7 +105,8 @@ sk_sp ImageFromCompressedData(fml::RefPtr descriptor, if (!descriptor->should_resize(target_width, target_height)) { // No resizing requested. Just decode & rasterize the image. - return descriptor->image()->makeRasterImage(); + sk_sp image = descriptor->image(); + return image ? image->makeRasterImage() : nullptr; } const SkISize source_dimensions = descriptor->image_info().dimensions(); diff --git a/lib/ui/plugins/callback_cache.cc b/lib/ui/plugins/callback_cache.cc index 0194eb507bcdd..218134504712f 100644 --- a/lib/ui/plugins/callback_cache.cc +++ b/lib/ui/plugins/callback_cache.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #include "flutter/lib/ui/plugins/callback_cache.h" diff --git a/lib/ui/spirv/transpiler.cc b/lib/ui/spirv/transpiler.cc index 01fc31329a0ab..3b1d8cf8e2746 100644 --- a/lib/ui/spirv/transpiler.cc +++ b/lib/ui/spirv/transpiler.cc @@ -58,6 +58,7 @@ class TranspilerImpl : public Transpiler { private: std::string ResolveName(uint32_t id); std::string ResolveType(uint32_t id); + size_t ResolveTypeFloatCount(uint32_t id); std::string ResolveGLSLName(uint32_t id); const spv_context spv_context_; diff --git a/lib/ui/text.dart b/lib/ui/text.dart index 8a03b68c68fa1..4e9c97cca2f95 100644 --- a/lib/ui/text.dart +++ b/lib/ui/text.dart @@ -2242,7 +2242,7 @@ class ParagraphBuilder extends NativeFieldWrapperClass2 { _placeholderCount++; _placeholderScales.add(scale); } - String _addPlaceholder(double width, double height, int alignment, double baselineOffset, int? baseline) native 'ParagraphBuilder_addPlaceholder'; + String? _addPlaceholder(double width, double height, int alignment, double baselineOffset, int? baseline) native 'ParagraphBuilder_addPlaceholder'; /// Applies the given paragraph style and returns a [Paragraph] containing the /// added text and associated styling. diff --git a/lib/ui/text/paragraph.cc b/lib/ui/text/paragraph.cc index 31897c82d9b3b..e8186be6c4067 100644 --- a/lib/ui/text/paragraph.cc +++ b/lib/ui/text/paragraph.cc @@ -132,20 +132,19 @@ tonic::Float32List Paragraph::getRectsForPlaceholders() { } Dart_Handle Paragraph::getPositionForOffset(double dx, double dy) { - Dart_Handle result = Dart_NewListOf(Dart_CoreType_Int, 2); txt::Paragraph::PositionWithAffinity pos = m_paragraph->GetGlyphPositionAtCoordinate(dx, dy); - Dart_ListSetAt(result, 0, ToDart(pos.position)); - Dart_ListSetAt(result, 1, ToDart(static_cast(pos.affinity))); - return result; + std::vector result = { + pos.position, // size_t already + static_cast(pos.affinity) // affinity (enum) + }; + return tonic::DartConverter::ToDart(result); } Dart_Handle Paragraph::getWordBoundary(unsigned offset) { txt::Paragraph::Range point = m_paragraph->GetWordBoundary(offset); - Dart_Handle result = Dart_NewListOf(Dart_CoreType_Int, 2); - Dart_ListSetAt(result, 0, ToDart(point.start)); - Dart_ListSetAt(result, 1, ToDart(point.end)); - return result; + std::vector result = {point.start, point.end}; + return tonic::DartConverter::ToDart(result); } Dart_Handle Paragraph::getLineBoundary(unsigned offset) { @@ -159,10 +158,8 @@ Dart_Handle Paragraph::getLineBoundary(unsigned offset) { break; } } - Dart_Handle result = Dart_NewListOf(Dart_CoreType_Int, 2); - Dart_ListSetAt(result, 0, ToDart(line_start)); - Dart_ListSetAt(result, 1, ToDart(line_end)); - return result; + std::vector result = {line_start, line_end}; + return tonic::DartConverter::ToDart(result); } tonic::Float64List Paragraph::computeLineMetrics() { diff --git a/lib/ui/text/text_box.h b/lib/ui/text/text_box.h index c3eea00aa8999..27f60670f232f 100644 --- a/lib/ui/text/text_box.h +++ b/lib/ui/text/text_box.h @@ -25,18 +25,4 @@ struct TextBox { } // namespace flutter -namespace tonic { - -template <> -struct DartConverter { - static Dart_Handle ToDart(const flutter::TextBox& val); -}; - -template <> -struct DartListFactory { - static Dart_Handle NewList(intptr_t length); -}; - -} // namespace tonic - #endif // FLUTTER_LIB_UI_TEXT_TEXT_BOX_H_ diff --git a/lib/ui/window/platform_configuration.h b/lib/ui/window/platform_configuration.h index 4652c7c5dcdbb..01089dac9c202 100644 --- a/lib/ui/window/platform_configuration.h +++ b/lib/ui/window/platform_configuration.h @@ -19,20 +19,6 @@ #include "flutter/lib/ui/window/window.h" #include "third_party/tonic/dart_persistent_value.h" -namespace tonic { -class DartLibraryNatives; - -// So tonic::ToDart> returns List instead of -// List. -template <> -struct DartListFactory { - static Dart_Handle NewList(intptr_t length) { - return Dart_NewListOf(Dart_CoreType_Int, length); - } -}; - -} // namespace tonic - namespace flutter { class FontCollection; class PlatformMessage; diff --git a/lib/web_ui/dev/driver_version.yaml b/lib/web_ui/dev/driver_version.yaml index f2c3baf9391f8..7da5245013348 100644 --- a/lib/web_ui/dev/driver_version.yaml +++ b/lib/web_ui/dev/driver_version.yaml @@ -1,6 +1,7 @@ ## Map for driver versions to use for each browser version. ## See: https://chromedriver.chromium.org/downloads chrome: + 86: '86.0.4240.22' 85: '85.0.4183.38' 84: '84.0.4147.30' 83: '83.0.4103.39' diff --git a/lib/web_ui/dev/goldens_lock.yaml b/lib/web_ui/dev/goldens_lock.yaml index 067577a642f6f..f558ffe268551 100644 --- a/lib/web_ui/dev/goldens_lock.yaml +++ b/lib/web_ui/dev/goldens_lock.yaml @@ -1,2 +1,2 @@ repository: https://github.com/flutter/goldens.git -revision: 1a4722227af42c3f51450266016b1a07ae459e73 +revision: 67f22ef933be27ba2be8b27df1b71b2c69eb86e5 diff --git a/lib/web_ui/dev/integration_tests_manager.dart b/lib/web_ui/dev/integration_tests_manager.dart index e01a632f219cd..ac6c2b50a6183 100644 --- a/lib/web_ui/dev/integration_tests_manager.dart +++ b/lib/web_ui/dev/integration_tests_manager.dart @@ -24,7 +24,10 @@ class IntegrationTestsManager { final DriverManager _driverManager; - IntegrationTestsManager(this._browser, this._useSystemFlutter) + final bool _doUpdateScreenshotGoldens; + + IntegrationTestsManager( + this._browser, this._useSystemFlutter, this._doUpdateScreenshotGoldens) : _driverManager = DriverManager.chooseDriver(_browser); Future runTests() async { @@ -159,14 +162,19 @@ class IntegrationTestsManager { Future _runTestsInProfileMode( io.Directory directory, String testName) async { - final String executable = + String executable = _useSystemFlutter ? 'flutter' : environment.flutterCommand.path; + Map enviroment = Map(); + if (_doUpdateScreenshotGoldens) { + enviroment['UPDATE_GOLDENS'] = 'true'; + } final IntegrationArguments arguments = IntegrationArguments.fromBrowser(_browser); final int exitCode = await runProcess( executable, arguments.getTestArguments(testName, 'profile'), workingDirectory: directory.path, + environment: enviroment, ); if (exitCode != 0) { @@ -334,7 +342,7 @@ class ChromeIntegrationArguments extends IntegrationArguments { '--$mode', '--browser-name=chrome', if (isLuci) '--chrome-binary=${preinstalledChromeExecutable()}', - if (isLuci) '--headless', + '--headless', '--local-engine=host_debug_unopt', ]; } diff --git a/lib/web_ui/dev/test_platform.dart b/lib/web_ui/dev/test_platform.dart index 18dd92cf1c45e..330457c23b403 100644 --- a/lib/web_ui/dev/test_platform.dart +++ b/lib/web_ui/dev/test_platform.dart @@ -22,6 +22,8 @@ import 'package:shelf_web_socket/shelf_web_socket.dart'; import 'package:shelf_packages_handler/shelf_packages_handler.dart'; import 'package:stream_channel/stream_channel.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; +import 'package:web_test_utils/goldens.dart'; +import 'package:web_test_utils/image_compare.dart'; import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports @@ -39,7 +41,6 @@ import 'package:test_core/src/runner/configuration.dart'; // ignore: implementat import 'browser.dart'; import 'common.dart'; import 'environment.dart' as env; -import 'goldens.dart'; import 'screenshot_manager.dart'; import 'supported_browsers.dart'; @@ -197,17 +198,6 @@ class BrowserPlatform extends PlatformPlugin { 'golden_files', ); } else { - // On LUCI MacOS bots the goldens are fetched by the recipe code. - // Fetch the goldens if: - // - Tests are running on a local machine. - // - Tests are running on an OS other than macOS. - if (!isLuci || !Platform.isMacOS) { - await fetchGoldens(); - } else { - if (!env.environment.webUiGoldensRepositoryDirectory.existsSync()) { - throw Exception('The goldens directory must have been copied'); - } - } goldensDirectory = p.join( env.environment.webUiGoldensRepositoryDirectory.path, 'engine', @@ -215,19 +205,6 @@ class BrowserPlatform extends PlatformPlugin { ); } - // Bail out fast if golden doesn't exist, and user doesn't want to create it. - final File file = File(p.join( - goldensDirectory, - filename, - )); - if (!file.existsSync() && !write) { - return ''' -Golden file $filename does not exist on path ${file.absolute.path} - -To automatically create this file call matchGoldenFile('$filename', write: true). -'''; - } - final Rectangle regionAsRectange = Rectangle( region['x'] as num, region['y'] as num, @@ -238,115 +215,14 @@ To automatically create this file call matchGoldenFile('$filename', write: true) // Take screenshot. final Image screenshot = await _screenshotManager.capture(regionAsRectange); - if (write) { - // Don't even bother with the comparison, just write and return - print('Updating screenshot golden: $file'); - file.writeAsBytesSync(encodePng(screenshot), flush: true); - if (doUpdateScreenshotGoldens) { - // Do not fail tests when bulk-updating screenshot goldens. - return 'OK'; - } else { - return 'Golden file $filename was updated. You can remove "write: true" in the call to matchGoldenFile.'; - } - } - - // Compare screenshots. - ImageDiff diff = ImageDiff( - golden: decodeNamedImage(file.readAsBytesSync(), filename), - other: screenshot, - pixelComparison: pixelComparison, - ); - - if (diff.rate > 0) { - final String testResultsPath = - env.environment.webUiTestResultsDirectory.path; - final String basename = p.basenameWithoutExtension(file.path); - - final File actualFile = - File(p.join(testResultsPath, '$basename.actual.png')); - actualFile.writeAsBytesSync(encodePng(screenshot), flush: true); - - final File diffFile = File(p.join(testResultsPath, '$basename.diff.png')); - diffFile.writeAsBytesSync(encodePng(diff.diff), flush: true); - - final File expectedFile = - File(p.join(testResultsPath, '$basename.expected.png')); - file.copySync(expectedFile.path); - - final File reportFile = - File(p.join(testResultsPath, '$basename.report.html')); - reportFile.writeAsStringSync(''' -Golden file $filename did not match the image generated by the test. - - - - - - - - - - - - -
ExpectedDiffActual
- - - - - -
-'''); - - final StringBuffer message = StringBuffer(); - message.writeln( - 'Golden file $filename did not match the image generated by the test.'); - message.writeln(getPrintableDiffFilesInfo(diff.rate, maxDiffRateFailure)); - message - .writeln('You can view the test report in your browser by opening:'); - - // Cirrus cannot serve HTML pages generated by build jobs, so we - // archive all the files so that they can be downloaded and inspected - // locally. - if (isCirrus) { - final String taskId = Platform.environment['CIRRUS_TASK_ID']; - final String baseArtifactsUrl = - 'https://api.cirrus-ci.com/v1/artifact/task/$taskId/web_engine_test/test_results'; - final String cirrusReportUrl = '$baseArtifactsUrl/$basename.report.zip'; - message.writeln(cirrusReportUrl); - - await Process.run( - 'zip', - [ - '$basename.report.zip', - '$basename.report.html', - '$basename.expected.png', - '$basename.diff.png', - '$basename.actual.png', - ], - workingDirectory: testResultsPath, - ); - } else { - final String localReportPath = '$testResultsPath/$basename.report.html'; - message.writeln(localReportPath); - } - - message.writeln( - 'To update the golden file call matchGoldenFile(\'$filename\', write: true).'); - message.writeln('Golden file: ${expectedFile.path}'); - message.writeln('Actual file: ${actualFile.path}'); - - if (diff.rate < maxDiffRateFailure) { - // Issue a warning but do not fail the test. - print('WARNING:'); - print(message); - return 'OK'; - } else { - // Fail test - return '$message'; - } - } - return 'OK'; + return compareImage( + screenshot, + doUpdateScreenshotGoldens, + filename, + pixelComparison, + maxDiffRateFailure, + goldensDirectory: goldensDirectory, + write: write); } /// A handler that serves wrapper files used to bootstrap tests. diff --git a/lib/web_ui/dev/test_runner.dart b/lib/web_ui/dev/test_runner.dart index 5ad1b3b53dfc3..9538a380ec58b 100644 --- a/lib/web_ui/dev/test_runner.dart +++ b/lib/web_ui/dev/test_runner.dart @@ -15,6 +15,7 @@ import 'package:test_core/src/runner/hack_register_platform.dart' import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports import 'package:test_core/src/executable.dart' as test; // ignore: implementation_imports +import 'package:web_test_utils/goldens.dart'; import 'common.dart'; import 'environment.dart'; @@ -102,6 +103,13 @@ class TestCommand extends Command with ArgUtils { /// How many dart2js build tasks are running at the same time. final Pool _pool = Pool(8); + /// Checks if test harness preparation (such as fetching the goldens, + /// creating test_results directory or starting ios-simulator) has been done. + /// + /// If unit tests already did these steps, integration tests do not have to + /// repeat them. + bool _testPreparationReady = false; + /// Check the flags to see what type of tests are requested. TestTypesRequested findTestType() { if (boolArg('unit-tests-only') && boolArg('integration-tests-only')) { @@ -157,7 +165,12 @@ class TestCommand extends Command with ArgUtils { } Future runIntegrationTests() async { - return IntegrationTestsManager(browser, useSystemFlutter).runTests(); + if(!_testPreparationReady) { + await _prepare(); + } + return IntegrationTestsManager( + browser, useSystemFlutter, doUpdateScreenshotGoldens) + .runTests(); } Future runUnitTests() async { @@ -189,11 +202,21 @@ class TestCommand extends Command with ArgUtils { } environment.webUiTestResultsDirectory.createSync(recursive: true); + // If screenshot tests are available, fetch the screenshot goldens. + if (isScreenshotTestsAvailable) { + print('screenshot tests available'); + final GoldensRepoFetcher goldensRepoFetcher = GoldensRepoFetcher( + environment.webUiGoldensRepositoryDirectory, + path.join(environment.webUiDevDir.path, 'goldens_lock.yaml')); + await goldensRepoFetcher.fetch(); + } + // In order to run iOS Safari unit tests we need to make sure iOS Simulator // is booted. if (isSafariIOS) { await IosSafariArgParser.instance.initIosSimulator(); } + _testPreparationReady = true; } /// Builds all test targets that will be run. @@ -371,6 +394,15 @@ class TestCommand extends Command with ArgUtils { isFirefoxIntegrationTestAvailable || isSafariIntegrationTestAvailable; + // Whether the tests will do screenshot testing. + bool get isScreenshotTestsAvailable => + isIntegrationTestsAvailable || isUnitTestsScreenshotsAvailable; + + // For unit tests screenshot tests and smoke tests only run on: + // "Chrome/iOS" for LUCI/local. + bool get isUnitTestsScreenshotsAvailable => + isChrome && (io.Platform.isLinux || !isLuci) || isSafariIOS; + /// Use system flutter instead of cloning the repository. /// /// Read the flag help for more details. Uses PATH to locate flutter. @@ -397,13 +429,7 @@ class TestCommand extends Command with ArgUtils { 'test', )); - // Screenshot tests and smoke tests only run on: "Chrome/iOS Safari" - // locally and on LUCI. They are not available on Windows bots: - // TODO: https://github.com/flutter/flutter/issues/63710 - if ((isChrome && isLuci && io.Platform.isLinux) || - ((isChrome || isSafariIOS) && !isLuci) || - (isSafariIOS && isLuci)) { - print('INFO: Also running the screenshot tests.'); + if (isUnitTestsScreenshotsAvailable) { // Separate screenshot tests from unit-tests. Screenshot tests must run // one at a time. Otherwise, they will end up screenshotting each other. // This is not an issue for unit-tests. @@ -621,7 +647,8 @@ class TestCommand extends Command with ArgUtils { /// Runs a batch of tests. /// - /// Unless [expectFailure] is set to false, sets [io.exitCode] to a non-zero value if any tests fail. + /// Unless [expectFailure] is set to false, sets [io.exitCode] to a non-zero + /// value if any tests fail. Future _runTestBatch( List testFiles, { @required int concurrency, @@ -644,7 +671,8 @@ class TestCommand extends Command with ArgUtils { return BrowserPlatform.start( browser, root: io.Directory.current.path, - // It doesn't make sense to update a screenshot for a test that is expected to fail. + // It doesn't make sense to update a screenshot for a test that is + // expected to fail. doUpdateScreenshotGoldens: !expectFailure && doUpdateScreenshotGoldens, ); }); diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index d8c01373840f8..8799a62ea4b19 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -26,7 +26,6 @@ part 'engine/alarm_clock.dart'; part 'engine/assets.dart'; part 'engine/bitmap_canvas.dart'; part 'engine/browser_detection.dart'; -part 'engine/browser_location.dart'; part 'engine/canvaskit/canvas.dart'; part 'engine/canvaskit/canvaskit_canvas.dart'; part 'engine/canvaskit/canvaskit_api.dart'; @@ -63,7 +62,9 @@ part 'engine/dom_canvas.dart'; part 'engine/dom_renderer.dart'; part 'engine/engine_canvas.dart'; part 'engine/frame_reference.dart'; -part 'engine/history.dart'; +part 'engine/navigation/history.dart'; +part 'engine/navigation/js_url_strategy.dart'; +part 'engine/navigation/url_strategy.dart'; part 'engine/html/backdrop_filter.dart'; part 'engine/html/canvas.dart'; part 'engine/html/clip.dart'; @@ -88,7 +89,9 @@ part 'engine/html/recording_canvas.dart'; part 'engine/html/render_vertices.dart'; part 'engine/html/scene.dart'; part 'engine/html/scene_builder.dart'; -part 'engine/html/shader.dart'; +part 'engine/html/shaders/normalized_gradient.dart'; +part 'engine/html/shaders/shader.dart'; +part 'engine/html/shaders/shader_builder.dart'; part 'engine/html/surface.dart'; part 'engine/html/surface_stats.dart'; part 'engine/html/transform.dart'; diff --git a/lib/web_ui/lib/src/engine/bitmap_canvas.dart b/lib/web_ui/lib/src/engine/bitmap_canvas.dart index 0f054559b18a0..557106a26412a 100644 --- a/lib/web_ui/lib/src/engine/bitmap_canvas.dart +++ b/lib/web_ui/lib/src/engine/bitmap_canvas.dart @@ -94,6 +94,16 @@ class BitmapCanvas extends EngineCanvas { _childOverdraw = value; } + /// Indicates bitmap canvas contains a 3d transform. + /// WebKit fails to preserve paint order when this happens and therefore + /// requires insertion of
to be + /// used for each child to force correct rendering order. + bool _contains3dTransform = false; + + /// Indicates that contents should be rendered into canvas so a dataUrl + /// can be constructed from contents. + bool _preserveImageData = false; + /// Allocates a canvas with enough memory to paint a picture within the given /// [bounds]. /// @@ -117,6 +127,13 @@ class BitmapCanvas extends EngineCanvas { _setupInitialTransform(); } + /// Constructs bitmap canvas to capture image data. + factory BitmapCanvas.imageData(ui.Rect bounds) { + BitmapCanvas bitmapCanvas = BitmapCanvas(bounds); + bitmapCanvas._preserveImageData = true; + return bitmapCanvas; + } + /// Setup cache for reusing DOM elements across frames. void setElementCache(CrossFrameCache cache) { _elementCache = cache; @@ -139,8 +156,9 @@ class BitmapCanvas extends EngineCanvas { final double canvasPositionCorrectionX = _bounds.left - BitmapCanvas.kPaddingPixels - _canvasPositionX!.toDouble(); - final double canvasPositionCorrectionY = - _bounds.top - BitmapCanvas.kPaddingPixels - _canvasPositionY!.toDouble(); + final double canvasPositionCorrectionY = _bounds.top - + BitmapCanvas.kPaddingPixels - + _canvasPositionY!.toDouble(); // This compensates for the translate on the `rootElement`. _canvasPool.initialTransform = ui.Offset( -_bounds.left + canvasPositionCorrectionX + BitmapCanvas.kPaddingPixels, @@ -175,6 +193,7 @@ class BitmapCanvas extends EngineCanvas { /// Prepare to reuse this canvas by clearing it's current contents. @override void clear() { + _contains3dTransform = false; _canvasPool.clear(); final int len = _children.length; for (int i = 0; i < len; i++) { @@ -208,8 +227,8 @@ class BitmapCanvas extends EngineCanvas { } /// Sets the global paint styles to correspond to [paint]. - void _setUpPaint(SurfacePaintData paint) { - _canvasPool.contextHandle.setUpPaint(paint); + void _setUpPaint(SurfacePaintData paint, ui.Rect? shaderBounds) { + _canvasPool.contextHandle.setUpPaint(paint, shaderBounds); } void _tearDownPaint() { @@ -267,12 +286,26 @@ class BitmapCanvas extends EngineCanvas { @override void transform(Float32List matrix4) { + TransformKind transformKind = transformKindOf(matrix4); + if (transformKind == TransformKind.complex) { + _contains3dTransform = true; + } _canvasPool.transform(matrix4); } @override - void clipRect(ui.Rect rect) { - _canvasPool.clipRect(rect); + void clipRect(ui.Rect rect, ui.ClipOp op) { + if (op == ui.ClipOp.difference) { + // Create 2 rectangles inside each other that represents + // clip area difference using even-odd fill rule. + final SurfacePath path = new SurfacePath(); + path.fillType = ui.PathFillType.evenOdd; + path.addRect(ui.Rect.fromLTWH(0, 0, _bounds.width, _bounds.height)); + path.addRect(rect); + _canvasPool.clipPath(path); + } else { + _canvasPool.clipRect(rect); + } } @override @@ -285,65 +318,187 @@ class BitmapCanvas extends EngineCanvas { _canvasPool.clipPath(path); } + /// Whether drawing operation should use DOM node instead of Canvas. + /// + /// - Perspective transforms are not supported by canvas and require + /// DOM to render correctly. + /// - Pictures typically have large rect/rounded rectangles as background + /// prefer DOM if canvas has not been allocated yet. + bool _useDomForRendering(SurfacePaintData paint) => + _preserveImageData == false && ( + _contains3dTransform || + (_canvasPool._canvas == null && + paint.maskFilter == null && + paint.shader == null && + paint.style != ui.PaintingStyle.stroke)); + @override void drawColor(ui.Color color, ui.BlendMode blendMode) { - _canvasPool.drawColor(color, blendMode); + final SurfacePaintData paintData = SurfacePaintData() + ..color = color + ..blendMode = blendMode; + if (_useDomForRendering(paintData)) { + drawRect(_computeScreenBounds(_canvasPool._currentTransform), paintData); + } else { + _canvasPool.drawColor(color, blendMode); + } } @override void drawLine(ui.Offset p1, ui.Offset p2, SurfacePaintData paint) { - _setUpPaint(paint); - _canvasPool.strokeLine(p1, p2); - _tearDownPaint(); + if (_useDomForRendering(paint)) { + final SurfacePath path = SurfacePath() + ..moveTo(p1.dx, p1.dy) + ..lineTo(p2.dx, p2.dy); + drawPath(path, paint); + } else { + ui.Rect? shaderBounds = (paint.shader != null) ? + ui.Rect.fromPoints(p1, p2) : null; + _setUpPaint(paint, shaderBounds); + _canvasPool.strokeLine(p1, p2); + _tearDownPaint(); + } } @override void drawPaint(SurfacePaintData paint) { - _setUpPaint(paint); - _canvasPool.fill(); - _tearDownPaint(); + if (_useDomForRendering(paint)) { + drawRect(_computeScreenBounds(_canvasPool._currentTransform), paint); + } else { + ui.Rect? shaderBounds = (paint.shader != null) ? + _computePictureBounds() : null; + _setUpPaint(paint, shaderBounds); + _canvasPool.fill(); + _tearDownPaint(); + } } @override void drawRect(ui.Rect rect, SurfacePaintData paint) { - _setUpPaint(paint); - _canvasPool.drawRect(rect, paint.style); - _tearDownPaint(); + if (_useDomForRendering(paint)) { + html.HtmlElement element = _buildDrawRectElement( + rect, paint, 'draw-rect', _canvasPool._currentTransform); + _drawElement( + element, + ui.Offset( + math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)), + paint); + } else { + _setUpPaint(paint, rect); + _canvasPool.drawRect(rect, paint.style); + _tearDownPaint(); + } + } + + /// Inserts a dom element at [offset] creating stack of divs for clipping + /// if required. + void _drawElement( + html.Element element, ui.Offset offset, SurfacePaintData paint) { + if (_canvasPool.isClipped) { + final List clipElements = _clipContent( + _canvasPool._clipStack!, + element, + ui.Offset.zero, + transformWithOffset(_canvasPool._currentTransform, offset)); + for (html.Element clipElement in clipElements) { + rootElement.append(clipElement); + _children.add(clipElement); + } + } else { + rootElement.append(element); + _children.add(element); + } + ui.BlendMode? blendMode = paint.blendMode; + if (blendMode != null) { + element.style.mixBlendMode = _stringForBlendMode(blendMode) ?? ''; + } } @override void drawRRect(ui.RRect rrect, SurfacePaintData paint) { - _setUpPaint(paint); + final ui.Rect rect = rrect.outerRect; + if (_useDomForRendering(paint)) { + html.HtmlElement element = _buildDrawRectElement( + rect, paint, 'draw-rrect', _canvasPool._currentTransform); + _applyRRectBorderRadius(element.style, rrect); + _drawElement( + element, + ui.Offset( + math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)), + paint); + } else { + _setUpPaint(paint, rrect.outerRect); _canvasPool.drawRRect(rrect, paint.style); - _tearDownPaint(); + _tearDownPaint(); + } } @override void drawDRRect(ui.RRect outer, ui.RRect inner, SurfacePaintData paint) { - _setUpPaint(paint); + _setUpPaint(paint, outer.outerRect); _canvasPool.drawDRRect(outer, inner, paint.style); _tearDownPaint(); } @override void drawOval(ui.Rect rect, SurfacePaintData paint) { - _setUpPaint(paint); - _canvasPool.drawOval(rect, paint.style); - _tearDownPaint(); + if (_useDomForRendering(paint)) { + html.HtmlElement element = _buildDrawRectElement( + rect, paint, 'draw-oval', _canvasPool._currentTransform); + _drawElement( + element, + ui.Offset( + math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)), + paint); + element.style.borderRadius = + '${(rect.width / 2.0)}px / ${(rect.height / 2.0)}px'; + } else { + _setUpPaint(paint, rect); + _canvasPool.drawOval(rect, paint.style); + _tearDownPaint(); + } } @override void drawCircle(ui.Offset c, double radius, SurfacePaintData paint) { - _setUpPaint(paint); - _canvasPool.drawCircle(c, radius, paint.style); - _tearDownPaint(); + ui.Rect rect = ui.Rect.fromCircle(center: c, radius: radius); + if (_useDomForRendering(paint)) { + html.HtmlElement element = _buildDrawRectElement( + rect, paint, 'draw-circle', _canvasPool._currentTransform); + _drawElement( + element, + ui.Offset( + math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)), + paint); + element.style.borderRadius = '50%'; + } else { + _setUpPaint(paint, paint.shader != null + ? ui.Rect.fromCircle(center: c, radius: radius) : null); + _canvasPool.drawCircle(c, radius, paint.style); + _tearDownPaint(); + } } @override void drawPath(ui.Path path, SurfacePaintData paint) { - _setUpPaint(paint); - _canvasPool.drawPath(path, paint.style); - _tearDownPaint(); + if (_useDomForRendering(paint)) { + final Matrix4 transform = _canvasPool._currentTransform; + final SurfacePath surfacePath = path as SurfacePath; + final ui.Rect pathBounds = surfacePath.getBounds(); + html.Element svgElm = _pathToSvgElement( + surfacePath, paint, '${pathBounds.right}', '${pathBounds.bottom}'); + if (!_canvasPool.isClipped) { + svgElm.style + ..transform = matrix4ToCssTransform(transform) + ..transformOrigin = '0 0 0' + ..position = 'absolute'; + } + _drawElement(svgElm, ui.Offset(0, 0), paint); + } else { + _setUpPaint(paint, paint.shader != null ? path.getBounds() : null); + _canvasPool.drawPath(path, paint.style); + _tearDownPaint(); + } } @override @@ -356,8 +511,8 @@ class BitmapCanvas extends EngineCanvas { void drawImage(ui.Image image, ui.Offset p, SurfacePaintData paint) { final html.HtmlElement imageElement = _drawImage(image, p, paint); if (paint.colorFilter != null) { - _applyTargetSize(imageElement, image.width.toDouble(), - image.height.toDouble()); + _applyTargetSize( + imageElement, image.width.toDouble(), image.height.toDouble()); } _childOverdraw = true; _canvasPool.closeCurrentCanvas(); @@ -367,7 +522,8 @@ class BitmapCanvas extends EngineCanvas { html.ImageElement _reuseOrCreateImage(HtmlImage htmlImage) { final String cacheKey = htmlImage.imgElement.src!; if (_elementCache != null) { - html.ImageElement? imageElement = _elementCache!.reuse(cacheKey) as html.ImageElement?; + html.ImageElement? imageElement = + _elementCache!.reuse(cacheKey) as html.ImageElement?; if (imageElement != null) { return imageElement; } @@ -388,7 +544,8 @@ class BitmapCanvas extends EngineCanvas { ui.Image image, ui.Offset p, SurfacePaintData paint) { final HtmlImage htmlImage = image as HtmlImage; final ui.BlendMode? blendMode = paint.blendMode; - final EngineColorFilter? colorFilter = paint.colorFilter as EngineColorFilter?; + final EngineColorFilter? colorFilter = + paint.colorFilter as EngineColorFilter?; final ui.BlendMode? colorFilterBlendMode = colorFilter?._blendMode; html.HtmlElement imgElement; if (colorFilterBlendMode == null) { @@ -409,21 +566,19 @@ class BitmapCanvas extends EngineCanvas { case ui.BlendMode.color: case ui.BlendMode.luminosity: case ui.BlendMode.xor: - imgElement = _createImageElementWithSvgFilter(image, - colorFilter!._color, colorFilterBlendMode, paint); + imgElement = _createImageElementWithSvgFilter( + image, colorFilter!._color, colorFilterBlendMode, paint); break; default: - imgElement = _createBackgroundImageWithBlend(image, - colorFilter!._color, colorFilterBlendMode, paint); + imgElement = _createBackgroundImageWithBlend( + image, colorFilter!._color, colorFilterBlendMode, paint); break; } } imgElement.style.mixBlendMode = _stringForBlendMode(blendMode) ?? ''; if (_canvasPool.isClipped) { // Reset width/height since they may have been previously set. - imgElement.style - ..removeProperty('width') - ..removeProperty('height'); + imgElement.style..removeProperty('width')..removeProperty('height'); final List clipElements = _clipContent( _canvasPool._clipStack!, imgElement, p, _canvasPool.currentTransform); for (html.Element clipElement in clipElements) { @@ -466,7 +621,7 @@ class BitmapCanvas extends EngineCanvas { } else { if (requiresClipping) { save(); - clipRect(dst); + clipRect(dst, ui.ClipOp.intersect); } double targetLeft = dst.left; double targetTop = dst.top; @@ -493,7 +648,8 @@ class BitmapCanvas extends EngineCanvas { targetWidth *= image.width / src.width; targetHeight *= image.height / src.height; } - _applyTargetSize(imgElement as html.HtmlElement, targetWidth, targetHeight); + _applyTargetSize( + imgElement as html.HtmlElement, targetWidth, targetHeight); if (requiresClipping) { restore(); } @@ -501,8 +657,8 @@ class BitmapCanvas extends EngineCanvas { _closeCurrentCanvas(); } - void _applyTargetSize(html.HtmlElement imageElement, double targetWidth, - double targetHeight) { + void _applyTargetSize( + html.HtmlElement imageElement, double targetWidth, double targetHeight) { final html.CssStyleDeclaration imageStyle = imageElement.style; final String widthPx = '${targetWidth.toStringAsFixed(2)}px'; final String heightPx = '${targetHeight.toStringAsFixed(2)}px'; @@ -532,8 +688,10 @@ class BitmapCanvas extends EngineCanvas { // For clear,dstOut it generates a blank element. // For src,srcOver it only sets background-color attribute. // For dst,dstIn , it only sets source not background color. - html.HtmlElement _createBackgroundImageWithBlend(HtmlImage image, - ui.Color? filterColor, ui.BlendMode colorFilterBlendMode, + html.HtmlElement _createBackgroundImageWithBlend( + HtmlImage image, + ui.Color? filterColor, + ui.BlendMode colorFilterBlendMode, SurfacePaintData paint) { // When blending with color we can't use an image element. // Instead use a div element with background image, color and @@ -548,8 +706,8 @@ class BitmapCanvas extends EngineCanvas { case ui.BlendMode.src: case ui.BlendMode.srcOver: style - ..position = 'absolute' - ..backgroundColor = colorToCssString(filterColor); + ..position = 'absolute' + ..backgroundColor = colorToCssString(filterColor); break; case ui.BlendMode.dst: case ui.BlendMode.dstIn: @@ -561,7 +719,8 @@ class BitmapCanvas extends EngineCanvas { style ..position = 'absolute' ..backgroundImage = "url('${image.imgElement.src}')" - ..backgroundBlendMode = _stringForBlendMode(colorFilterBlendMode) ?? '' + ..backgroundBlendMode = + _stringForBlendMode(colorFilterBlendMode) ?? '' ..backgroundColor = colorToCssString(filterColor); break; } @@ -569,12 +728,14 @@ class BitmapCanvas extends EngineCanvas { } // Creates an image element and an svg filter to apply on the element. - html.HtmlElement _createImageElementWithSvgFilter(HtmlImage image, - ui.Color? filterColor, ui.BlendMode colorFilterBlendMode, + html.HtmlElement _createImageElementWithSvgFilter( + HtmlImage image, + ui.Color? filterColor, + ui.BlendMode colorFilterBlendMode, SurfacePaintData paint) { // For srcIn blendMode, we use an svg filter to apply to image element. - String? svgFilter = svgFilterFromBlendMode(filterColor, - colorFilterBlendMode); + String? svgFilter = + svgFilterFromBlendMode(filterColor, colorFilterBlendMode); final html.Element filterElement = html.Element.html(svgFilter, treeSanitizer: _NullTreeSanitizer()); rootElement.append(filterElement); @@ -641,9 +802,11 @@ class BitmapCanvas extends EngineCanvas { if (paragraph._drawOnCanvas && _childOverdraw == false) { // !Do not move this assignment above this if clause since, accessing // context will generate extra tags. - final List lines = paragraph._measurementResult!.lines!; + final List lines = + paragraph._measurementResult!.lines!; - final SurfacePaintData? backgroundPaint = paragraph._background?.paintData; + final SurfacePaintData? backgroundPaint = + paragraph._background?.paintData; if (backgroundPaint != null) { final ui.Rect rect = ui.Rect.fromLTWH( offset.dx, offset.dy, paragraph.width, paragraph.height); @@ -655,7 +818,7 @@ class BitmapCanvas extends EngineCanvas { ctx.font = style.cssFontString; _cachedLastStyle = style; } - _setUpPaint(paragraph._paint!.paintData); + _setUpPaint(paragraph._paint!.paintData, null); double y = offset.dy + paragraph.alphabeticBaseline; final int len = lines.length; for (int i = 0; i < len; i++) { @@ -713,8 +876,8 @@ class BitmapCanvas extends EngineCanvas { /// If colors is specified, convert colors to premultiplied (alpha) colors /// and use a SkTriColorShader to render. @override - void drawVertices( - SurfaceVertices vertices, ui.BlendMode blendMode, SurfacePaintData paint) { + void drawVertices(SurfaceVertices vertices, ui.BlendMode blendMode, + SurfacePaintData paint) { // TODO(flutter_web): Implement shaders for [Paint.shader] and // blendMode. https://github.com/flutter/flutter/issues/40096 // Move rendering to OffscreenCanvas so that transform is preserved @@ -751,7 +914,8 @@ class BitmapCanvas extends EngineCanvas { ..blendMode = ui.BlendMode.srcOver; @override - void drawPoints(ui.PointMode pointMode, Float32List points, SurfacePaintData paint) { + void drawPoints( + ui.PointMode pointMode, Float32List points, SurfacePaintData paint) { if (pointMode == ui.PointMode.points) { _drawPointsPaint.style = ui.PaintingStyle.stroke; } else { @@ -761,7 +925,7 @@ class BitmapCanvas extends EngineCanvas { _drawPointsPaint.strokeWidth = paint.strokeWidth; _drawPointsPaint.maskFilter = paint.maskFilter; - _setUpPaint(_drawPointsPaint); + _setUpPaint(_drawPointsPaint, null); _canvasPool.drawPoints(pointMode, points, paint.strokeWidth! / 2.0); _tearDownPaint(); } @@ -770,6 +934,50 @@ class BitmapCanvas extends EngineCanvas { void endOfPaint() { _canvasPool.endOfPaint(); _elementCache?.commitFrame(); + // Wrap all elements in translate3d (workaround for webkit paint order bug). + if (_contains3dTransform && browserEngine == BrowserEngine.webkit) { + for (html.Element element in rootElement.children) { + html.DivElement paintOrderElement = html.DivElement() + ..style.transform = 'translate3d(0,0,0)'; + paintOrderElement.append(element); + rootElement.append(paintOrderElement); + _children.add(paintOrderElement); + } + } + if (rootElement.firstChild is html.HtmlElement && + (rootElement.firstChild as html.HtmlElement).tagName.toLowerCase() == + 'canvas') { + (rootElement.firstChild as html.HtmlElement).style.zIndex = '-1'; + } + } + + /// Computes paint bounds given [targetTransform] to completely cover window + /// viewport. + ui.Rect _computeScreenBounds(Matrix4 targetTransform) { + final Matrix4 inverted = targetTransform.clone()..invert(); + final double dpr = ui.window.devicePixelRatio; + final double width = ui.window.physicalSize.width * dpr; + final double height = ui.window.physicalSize.height * dpr; + Vector3 topLeft = inverted.perspectiveTransform(Vector3(0, 0, 0)); + Vector3 topRight = inverted.perspectiveTransform(Vector3(width, 0, 0)); + Vector3 bottomRight = + inverted.perspectiveTransform(Vector3(width, height, 0)); + Vector3 bottomLeft = inverted.perspectiveTransform(Vector3(0, height, 0)); + return ui.Rect.fromLTRB( + math.min(topLeft.x, + math.min(topRight.x, math.min(bottomRight.x, bottomLeft.x))), + math.min(topLeft.y, + math.min(topRight.y, math.min(bottomRight.y, bottomLeft.y))), + math.max(topLeft.x, + math.max(topRight.x, math.max(bottomRight.x, bottomLeft.x))), + math.max(topLeft.y, + math.max(topRight.y, math.max(bottomRight.y, bottomLeft.y))), + ); + } + + /// Computes paint bounds to completely cover picture. + ui.Rect _computePictureBounds() { + return ui.Rect.fromLTRB(0, 0, _bounds.width, _bounds.height); } } @@ -875,7 +1083,7 @@ String _stringForStrokeJoin(ui.StrokeJoin strokeJoin) { /// it's contents. The clipping rectangles are nested and returned together /// with a list of svg elements that provide clip-paths. List _clipContent(List<_SaveClipEntry> clipStack, - html.HtmlElement content, ui.Offset offset, Matrix4 currentTransform) { + html.Element content, ui.Offset offset, Matrix4 currentTransform) { html.Element? root, curElement; final List clipDefs = []; final int len = clipStack.length; @@ -892,6 +1100,9 @@ List _clipContent(List<_SaveClipEntry> clipStack, curElement = newElement; final ui.Rect? rect = entry.rect; Matrix4 newClipTransform = entry.currentTransform; + final TransformKind transformKind = + transformKindOf(newClipTransform.storage); + bool requiresTransformStyle = transformKind == TransformKind.complex; if (rect != null) { final double clipOffsetX = rect.left; final double clipOffsetY = rect.top; @@ -921,7 +1132,8 @@ List _clipContent(List<_SaveClipEntry> clipStack, curElement.style ..transform = matrix4ToCssTransform(newClipTransform) ..transformOrigin = '0 0 0'; - String svgClipPath = createSvgClipDef(curElement as html.HtmlElement, entry.path!); + String svgClipPath = + createSvgClipDef(curElement as html.HtmlElement, entry.path!); final html.Element clipElement = html.Element.html(svgClipPath, treeSanitizer: _NullTreeSanitizer()); clipDefs.add(clipElement); @@ -936,6 +1148,11 @@ List _clipContent(List<_SaveClipEntry> clipStack, reverseTransformDiv, (newClipTransform.clone()..invert()).storage, ); + if (requiresTransformStyle) { + // Instead of flattening matrix3d, preserve so it can be reversed. + curElement.style.transformStyle = 'preserve-3d'; + reverseTransformDiv.style.transformStyle = 'preserve-3d'; + } curElement.append(reverseTransformDiv); curElement = reverseTransformDiv; } @@ -965,4 +1182,3 @@ String _maskFilterToCanvasFilter(ui.MaskFilter? maskFilter) { return 'none'; } } - diff --git a/lib/web_ui/lib/src/engine/browser_location.dart b/lib/web_ui/lib/src/engine/browser_location.dart deleted file mode 100644 index a9701cd99060f..0000000000000 --- a/lib/web_ui/lib/src/engine/browser_location.dart +++ /dev/null @@ -1,211 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// @dart = 2.10 -part of engine; - -// TODO(mdebbar): add other strategies. - -// Some parts of this file were inspired/copied from the AngularDart router. - -/// [LocationStrategy] is responsible for representing and reading route state -/// from the browser's URL. -/// -/// At the moment, only one strategy is implemented: [HashLocationStrategy]. -/// -/// This is used by [BrowserHistory] to interact with browser history APIs. -abstract class LocationStrategy { - const LocationStrategy(); - - /// Subscribes to popstate events and returns a function that could be used to - /// unsubscribe from popstate events. - ui.VoidCallback onPopState(html.EventListener fn); - - /// The active path in the browser history. - String get path; - - /// The state of the current browser history entry. - dynamic get state; - - /// Given a path that's internal to the app, create the external url that - /// will be used in the browser. - String prepareExternalUrl(String internalUrl); - - /// Push a new history entry. - void pushState(dynamic state, String title, String url); - - /// Replace the currently active history entry. - void replaceState(dynamic state, String title, String url); - - /// Go to the previous history entry. - Future back({int count = 1}); -} - -/// This is an implementation of [LocationStrategy] that uses the browser URL's -/// [hash fragments](https://en.wikipedia.org/wiki/Uniform_Resource_Locator#Syntax) -/// to represent its state. -/// -/// In order to use this [LocationStrategy] for an app, it needs to be set in -/// [ui.window.locationStrategy]: -/// -/// ```dart -/// import 'package:flutter_web/material.dart'; -/// import 'package:flutter_web/ui.dart' as ui; -/// -/// void main() { -/// ui.window.locationStrategy = const ui.HashLocationStrategy(); -/// runApp(MyApp()); -/// } -/// ``` -class HashLocationStrategy extends LocationStrategy { - final PlatformLocation _platformLocation; - - const HashLocationStrategy( - [this._platformLocation = const BrowserPlatformLocation()]); - - @override - ui.VoidCallback onPopState(html.EventListener fn) { - _platformLocation.onPopState(fn); - return () => _platformLocation.offPopState(fn); - } - - @override - String get path { - // the hash value is always prefixed with a `#` - // and if it is empty then it will stay empty - String path = _platformLocation.hash ?? ''; - assert(path.isEmpty || path.startsWith('#')); - - // We don't want to return an empty string as a path. Instead we default to "/". - if (path.isEmpty || path == '#') { - return '/'; - } - // At this point, we know [path] starts with "#" and isn't empty. - return path.substring(1); - } - - @override - dynamic get state => _platformLocation.state; - - @override - String prepareExternalUrl(String internalUrl) { - // It's convention that if the hash path is empty, we omit the `#`; however, - // if the empty URL is pushed it won't replace any existing fragment. So - // when the hash path is empty, we instead return the location's path and - // query. - return internalUrl.isEmpty - ? '${_platformLocation.pathname}${_platformLocation.search}' - : '#$internalUrl'; - } - - @override - void pushState(dynamic state, String title, String url) { - _platformLocation.pushState(state, title, prepareExternalUrl(url)); - } - - @override - void replaceState(dynamic state, String title, String url) { - _platformLocation.replaceState(state, title, prepareExternalUrl(url)); - } - - @override - Future back({int count = 1}) { - _platformLocation.back(count); - return _waitForPopState(); - } - - /// Waits until the next popstate event is fired. - /// - /// This is useful for example to wait until the browser has handled the - /// `history.back` transition. - Future _waitForPopState() { - final Completer completer = Completer(); - late ui.VoidCallback unsubscribe; - unsubscribe = onPopState((_) { - unsubscribe(); - completer.complete(); - }); - return completer.future; - } -} - -/// [PlatformLocation] encapsulates all calls to DOM apis, which allows the -/// [LocationStrategy] classes to be platform agnostic and testable. -/// -/// The [PlatformLocation] class is used directly by all implementations of -/// [LocationStrategy] when they need to interact with the DOM apis like -/// pushState, popState, etc... -abstract class PlatformLocation { - const PlatformLocation(); - - void onPopState(html.EventListener fn); - void offPopState(html.EventListener fn); - - void onHashChange(html.EventListener fn); - void offHashChange(html.EventListener fn); - - String get pathname; - String get search; - String? get hash; - dynamic get state; - - void pushState(dynamic state, String title, String url); - void replaceState(dynamic state, String title, String url); - void back(int count); -} - -/// An implementation of [PlatformLocation] for the browser. -class BrowserPlatformLocation extends PlatformLocation { - html.Location get _location => html.window.location; - html.History get _history => html.window.history; - - const BrowserPlatformLocation(); - - @override - void onPopState(html.EventListener fn) { - html.window.addEventListener('popstate', fn); - } - - @override - void offPopState(html.EventListener fn) { - html.window.removeEventListener('popstate', fn); - } - - @override - void onHashChange(html.EventListener fn) { - html.window.addEventListener('hashchange', fn); - } - - @override - void offHashChange(html.EventListener fn) { - html.window.removeEventListener('hashchange', fn); - } - - @override - String get pathname => _location.pathname!; - - @override - String get search => _location.search!; - - @override - String get hash => _location.hash; - - @override - dynamic get state => _history.state; - - @override - void pushState(dynamic state, String title, String url) { - _history.pushState(state, title, url); - } - - @override - void replaceState(dynamic state, String title, String url) { - _history.replaceState(state, title, url); - } - - @override - void back(int count) { - _history.go(-count); - } -} diff --git a/lib/web_ui/lib/src/engine/canvas_pool.dart b/lib/web_ui/lib/src/engine/canvas_pool.dart index d56351124eed6..054ac12953acf 100644 --- a/lib/web_ui/lib/src/engine/canvas_pool.dart +++ b/lib/web_ui/lib/src/engine/canvas_pool.dart @@ -33,8 +33,6 @@ class _CanvasPool extends _SaveStackTracking { html.HtmlElement? _rootElement; int _saveContextCount = 0; - // Number of elements that have been added to flt-canvas. - int _activeElementCount = 0; _CanvasPool(this._widthInBitmapPixels, this._heightInBitmapPixels); @@ -76,7 +74,6 @@ class _CanvasPool extends _SaveStackTracking { _context = null; _contextHandle = null; } - _activeElementCount++; } void allocateCanvas(html.HtmlElement rootElement) { @@ -134,15 +131,12 @@ class _CanvasPool extends _SaveStackTracking { _rootElement!.append(canvas); } - if (_activeElementCount == 0) { - canvas.style.zIndex = '-1'; - } else if (reused) { - // If a canvas is the first element we set z-index = -1 to workaround - // blink compositing bug. To make sure this does not leak when reused - // reset z-index. + if (reused) { + // If a canvas is the first element we set z-index = -1 in [BitmapCanvas] + // endOfPaint to workaround blink compositing bug. To make sure this + // does not leak when reused reset z-index. canvas.style.removeProperty('z-index'); } - ++_activeElementCount; final html.CanvasRenderingContext2D context = _context = canvas.context2D; _contextHandle = ContextStateHandle(this, context); @@ -210,8 +204,13 @@ class _CanvasPool extends _SaveStackTracking { } else if (clipEntry.rrect != null) { _clipRRect(ctx, clipEntry.rrect!); } else if (clipEntry.path != null) { - _runPath(ctx, clipEntry.path as SurfacePath); - ctx.clip(); + final SurfacePath path = clipEntry.path as SurfacePath; + _runPath(ctx, path); + if (path.fillType == ui.PathFillType.nonZero) { + ctx.clip(); + } else { + ctx.clip('evenodd'); + } } } } @@ -265,7 +264,6 @@ class _CanvasPool extends _SaveStackTracking { _canvas = null; _context = null; _contextHandle = null; - _activeElementCount = 0; } void endOfPaint() { @@ -321,7 +319,7 @@ class _CanvasPool extends _SaveStackTracking { // Returns a "data://" URI containing a representation of the image in this // canvas in PNG format. - String toDataUrl() => _canvas!.toDataUrl(); + String toDataUrl() => _canvas?.toDataUrl() ?? ''; @override void save() { @@ -443,7 +441,11 @@ class _CanvasPool extends _SaveStackTracking { if (_canvas != null) { html.CanvasRenderingContext2D ctx = context; _runPath(ctx, path as SurfacePath); - ctx.clip(); + if (path.fillType == ui.PathFillType.nonZero) { + ctx.clip(); + } else { + ctx.clip('evenodd'); + } } } @@ -761,7 +763,7 @@ class ContextStateHandle { /// Sets paint properties on the current canvas. /// /// [tearDownPaint] must be called after calling this method. - void setUpPaint(SurfacePaintData paint) { + void setUpPaint(SurfacePaintData paint, ui.Rect? shaderBounds) { if (assertionsEnabled) { assert(!_debugIsPaintSetUp); _debugIsPaintSetUp = true; @@ -776,7 +778,7 @@ class ContextStateHandle { if (paint.shader != null) { final EngineGradient engineShader = paint.shader as EngineGradient; final Object paintStyle = - engineShader.createPaintStyle(_canvasPool.context); + engineShader.createPaintStyle(_canvasPool.context, shaderBounds); fillStyle = paintStyle; strokeStyle = paintStyle; } else if (paint.color != null) { diff --git a/lib/web_ui/lib/src/engine/canvaskit/canvas.dart b/lib/web_ui/lib/src/engine/canvaskit/canvas.dart index 3426253473469..06d325e3c1dfa 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/canvas.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvas.dart @@ -176,8 +176,7 @@ class CkCanvas { skCanvas.drawPicture(picture.skiaObject.skiaObject); } - void drawPoints(CkPaint paint, ui.PointMode pointMode, - Float32List points) { + void drawPoints(CkPaint paint, ui.PointMode pointMode, Float32List points) { skCanvas.drawPoints( toSkPointMode(pointMode), points, @@ -230,24 +229,24 @@ class CkCanvas { void saveLayer(ui.Rect bounds, CkPaint paint) { skCanvas.saveLayer( - toSkRect(bounds), paint.skiaObject, + toSkRect(bounds), + null, + null, ); } void saveLayerWithoutBounds(CkPaint paint) { - final SkCanvasSaveLayerWithoutBoundsOverload override = skCanvas as SkCanvasSaveLayerWithoutBoundsOverload; - override.saveLayer(paint.skiaObject); + skCanvas.saveLayer(paint.skiaObject, null, null, null); } void saveLayerWithFilter(ui.Rect bounds, ui.ImageFilter filter) { - final SkCanvasSaveLayerWithFilterOverload override = skCanvas as SkCanvasSaveLayerWithFilterOverload; final CkImageFilter skImageFilter = filter as CkImageFilter; - return override.saveLayer( + return skCanvas.saveLayer( null, + toSkRect(bounds), skImageFilter.skiaObject, 0, - toSkRect(bounds), ); } diff --git a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart index 1252cba16e436..09d67dbe25cbe 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -79,6 +79,10 @@ class CanvasKit { external int get LineThroughDecoration; // End of text decoration enum. + external SkTextDecorationStyleEnum get DecorationStyle; + external SkTextBaselineEnum get TextBaseline; + external SkPlaceholderAlignmentEnum get PlaceholderAlignment; + external SkFontMgrNamespace get SkFontMgr; external TypefaceFontProviderNamespace get TypefaceFontProvider; external int GetWebGLContext( @@ -1015,12 +1019,12 @@ class SkPath { external SkPath([SkPath? other]); external void setFillType(SkFillType fillType); external void addArc( - SkRect oval, + Float32List oval, double startAngleDegrees, double sweepAngleDegrees, ); external void addOval( - SkRect oval, + Float32List oval, bool counterClockWise, int startIndex, ); @@ -1041,16 +1045,15 @@ class SkPath { Float32List points, bool close, ); - external void addRoundRect( - SkRect outerRect, - Float32List radii, + external void addRRect( + Float32List rrect, bool counterClockWise, ); external void addRect( - SkRect rect, + Float32List rect, ); external void arcToOval( - SkRect oval, + Float32List oval, double startAngleDegrees, double sweepAngleDegrees, bool forceMoveTo, @@ -1084,7 +1087,7 @@ class SkPath { double x3, double y3, ); - external SkRect getBounds(); + external Float32List getBounds(); external void lineTo(double x, double y); external void moveTo(double x, double y); external void quadTo( @@ -1156,88 +1159,46 @@ class SkContourMeasure { external double length(); } -@JS() -@anonymous -class SkRect { - external factory SkRect({ - required double fLeft, - required double fTop, - required double fRight, - required double fBottom, - }); - external double get fLeft; - external double get fTop; - external double get fRight; - external double get fBottom; -} - -extension SkRectExtensions on SkRect { - ui.Rect toRect() { - return ui.Rect.fromLTRB( - this.fLeft, - this.fTop, - this.fRight, - this.fBottom, - ); - } -} - -SkRect toSkRect(ui.Rect rect) { - return SkRect( - fLeft: rect.left, - fTop: rect.top, - fRight: rect.right, - fBottom: rect.bottom, - ); -} - -@JS() -@anonymous -class SkRRect { - external factory SkRRect({ - required SkRect rect, - required double rx1, - required double ry1, - required double rx2, - required double ry2, - required double rx3, - required double ry3, - required double rx4, - required double ry4, - }); - - external SkRect get rect; - external double get rx1; - external double get ry1; - external double get rx2; - external double get ry2; - external double get rx3; - external double get ry3; - external double get rx4; - external double get ry4; -} - -SkRRect toSkRRect(ui.RRect rrect) { - return SkRRect( - rect: toOuterSkRect(rrect), - rx1: rrect.tlRadiusX, - ry1: rrect.tlRadiusY, - rx2: rrect.trRadiusX, - ry2: rrect.trRadiusY, - rx3: rrect.brRadiusX, - ry3: rrect.brRadiusY, - rx4: rrect.blRadiusX, - ry4: rrect.blRadiusY, - ); -} - -SkRect toOuterSkRect(ui.RRect rrect) { - return SkRect( - fLeft: rrect.left, - fTop: rrect.top, - fRight: rrect.right, - fBottom: rrect.bottom, - ); +// TODO(hterkelsen): Use a shared malloc'ed array for performance. +Float32List toSkRect(ui.Rect rect) { + final Float32List skRect = Float32List(4); + skRect[0] = rect.left; + skRect[1] = rect.top; + skRect[2] = rect.right; + skRect[3] = rect.bottom; + return skRect; +} + +ui.Rect fromSkRect(Float32List skRect) { + return ui.Rect.fromLTRB(skRect[0], skRect[1], skRect[2], skRect[3]); +} + +// TODO(hterkelsen): Use a shared malloc'ed array for performance. +Float32List toSkRRect(ui.RRect rrect) { + final Float32List skRRect = Float32List(12); + skRRect[0] = rrect.left; + skRRect[1] = rrect.top; + skRRect[2] = rrect.right; + skRRect[3] = rrect.bottom; + skRRect[4] = rrect.tlRadiusX; + skRRect[5] = rrect.tlRadiusY; + skRRect[6] = rrect.trRadiusX; + skRRect[7] = rrect.trRadiusY; + skRRect[8] = rrect.brRadiusX; + skRRect[9] = rrect.brRadiusY; + skRRect[10] = rrect.blRadiusX; + skRRect[11] = rrect.blRadiusY; + return skRRect; +} + +// TODO(hterkelsen): Use a shared malloc'ed array for performance. +Float32List toOuterSkRect(ui.RRect rrect) { + final Float32List skRect = Float32List(4); + skRect[0] = rrect.left; + skRect[1] = rrect.top; + skRect[2] = rrect.right; + skRect[3] = rrect.bottom; + return skRect; } /// Encodes a list of offsets to CanvasKit-compatible point array. @@ -1263,9 +1224,9 @@ List rawPointsToSkPoints2d(Float32List points) { assert(points.length % 2 == 0); final int pointLength = points.length ~/ 2; final List result = []; - for (var i = 0; i < pointLength; i++) { - var x = i * 2; - var y = x + 1; + for (int i = 0; i < pointLength; i++) { + int x = i * 2; + int y = x + 1; final Float32List skPoint = Float32List(2); skPoint[0] = points[x]; skPoint[1] = points[y]; @@ -1277,7 +1238,7 @@ List rawPointsToSkPoints2d(Float32List points) { List toSkPoints2d(List offsets) { final int len = offsets.length; final List result = []; - for (var i = 0; i < len; i++) { + for (int i = 0; i < len; i++) { final ui.Offset offset = offsets[i]; final Float32List skPoint = Float32List(2); skPoint[0] = offset.dx; @@ -1299,7 +1260,7 @@ Uint16List toUint16List(List ints) { @JS('window.flutterCanvasKit.SkPictureRecorder') class SkPictureRecorder { external SkPictureRecorder(); - external SkCanvas beginRecording(SkRect bounds); + external SkCanvas beginRecording(Float32List bounds); external SkPicture finishRecordingAsPicture(); external void delete(); } @@ -1319,17 +1280,17 @@ class SkCanvas { bool doAntiAlias, ); external void clipRRect( - SkRRect rrect, + Float32List rrect, SkClipOp clipOp, bool doAntiAlias, ); external void clipRect( - SkRect rrect, + Float32List rrect, SkClipOp clipOp, bool doAntiAlias, ); external void drawArc( - SkRect oval, + Float32List oval, double startAngleDegrees, double sweepAngleDegrees, bool useCenter, @@ -1354,8 +1315,8 @@ class SkCanvas { SkBlendMode blendMode, ); external void drawDRRect( - SkRRect outer, - SkRRect inner, + Float32List outer, + Float32List inner, SkPaint paint, ); external void drawImage( @@ -1366,15 +1327,15 @@ class SkCanvas { ); external void drawImageRect( SkImage image, - SkRect src, - SkRect dst, + Float32List src, + Float32List dst, SkPaint paint, bool fastSample, ); external void drawImageNine( SkImage image, - SkRect center, - SkRect dst, + Float32List center, + Float32List dst, SkPaint paint, ); external void drawLine( @@ -1385,7 +1346,7 @@ class SkCanvas { SkPaint paint, ); external void drawOval( - SkRect rect, + Float32List rect, SkPaint paint, ); external void drawPaint( @@ -1401,11 +1362,11 @@ class SkCanvas { SkPaint paint, ); external void drawRRect( - SkRRect rrect, + Float32List rrect, SkPaint paint, ); external void drawRect( - SkRect rrect, + Float32List rrect, SkPaint paint, ); external void drawShadow( @@ -1425,8 +1386,10 @@ class SkCanvas { external int save(); external int getSaveCount(); external void saveLayer( - SkRect bounds, - SkPaint paint, + SkPaint? paint, + Float32List? bounds, + SkImageFilter? backdrop, + int? flags, ); external void restore(); external void restoreToCount(int count); @@ -1448,23 +1411,6 @@ class SkCanvas { ); } -@JS() -@anonymous -class SkCanvasSaveLayerWithoutBoundsOverload { - external void saveLayer(SkPaint paint); -} - -@JS() -@anonymous -class SkCanvasSaveLayerWithFilterOverload { - external void saveLayer( - SkPaint? paint, - SkImageFilter? imageFilter, - int flags, - SkRect rect, - ); -} - @JS() @anonymous class SkPicture { @@ -1493,6 +1439,7 @@ class SkParagraphBuilder { external void pushPaintStyle( SkTextStyle textStyle, SkPaint foreground, SkPaint background); external void pop(); + external void addPlaceholder(SkPlaceholderStyleProperties placeholderStyle); external SkParagraph build(); external void delete(); } @@ -1504,69 +1451,162 @@ class SkParagraphStyle {} @JS() @anonymous class SkParagraphStyleProperties { - external SkTextAlign? get textAlign; external set textAlign(SkTextAlign? value); - - external SkTextDirection? get textDirection; external set textDirection(SkTextDirection? value); - - external double? get heightMultiplier; external set heightMultiplier(double? value); - - external int? get textHeightBehavior; external set textHeightBehavior(int? value); - - external int? get maxLines; external set maxLines(int? value); - - external String? get ellipsis; external set ellipsis(String? value); - - external SkTextStyleProperties? get textStyle; external set textStyle(SkTextStyleProperties? value); + external set strutStyle(SkStrutStyleProperties? strutStyle); } @JS() class SkTextStyle {} +@JS() +class SkTextDecorationStyleEnum { + external SkTextDecorationStyle get Solid; + external SkTextDecorationStyle get Double; + external SkTextDecorationStyle get Dotted; + external SkTextDecorationStyle get Dashed; + external SkTextDecorationStyle get Wavy; +} + +@JS() +class SkTextDecorationStyle { + external int get value; +} + +final List _skTextDecorationStyles = + [ + canvasKit.DecorationStyle.Solid, + canvasKit.DecorationStyle.Double, + canvasKit.DecorationStyle.Dotted, + canvasKit.DecorationStyle.Dashed, + canvasKit.DecorationStyle.Wavy, +]; + +SkTextDecorationStyle toSkTextDecorationStyle(ui.TextDecorationStyle style) { + return _skTextDecorationStyles[style.index]; +} + +@JS() +class SkTextBaselineEnum { + external SkTextBaseline get Alphabetic; + external SkTextBaseline get Ideographic; +} + +@JS() +class SkTextBaseline { + external int get value; +} + +final List _skTextBaselines = [ + canvasKit.TextBaseline.Alphabetic, + canvasKit.TextBaseline.Ideographic, +]; + +SkTextBaseline toSkTextBaseline(ui.TextBaseline baseline) { + return _skTextBaselines[baseline.index]; +} + +@JS() +class SkPlaceholderAlignmentEnum { + external SkPlaceholderAlignment get Baseline; + external SkPlaceholderAlignment get AboveBaseline; + external SkPlaceholderAlignment get BelowBaseline; + external SkPlaceholderAlignment get Top; + external SkPlaceholderAlignment get Bottom; + external SkPlaceholderAlignment get Middle; +} + +@JS() +class SkPlaceholderAlignment { + external int get value; +} + +final List _skPlaceholderAlignments = + [ + canvasKit.PlaceholderAlignment.Baseline, + canvasKit.PlaceholderAlignment.AboveBaseline, + canvasKit.PlaceholderAlignment.BelowBaseline, + canvasKit.PlaceholderAlignment.Top, + canvasKit.PlaceholderAlignment.Bottom, + canvasKit.PlaceholderAlignment.Middle, +]; + +SkPlaceholderAlignment toSkPlaceholderAlignment( + ui.PlaceholderAlignment alignment) { + return _skPlaceholderAlignments[alignment.index]; +} + @JS() @anonymous class SkTextStyleProperties { - external Float32List? get backgroundColor; external set backgroundColor(Float32List? value); - - external Float32List? get color; external set color(Float32List? value); - - external Float32List? get foregroundColor; external set foregroundColor(Float32List? value); - - external int? get decoration; external set decoration(int? value); - - external double? get decorationThickness; external set decorationThickness(double? value); - - external double? get fontSize; + external set decorationColor(Float32List? value); + external set decorationStyle(SkTextDecorationStyle? value); + external set textBaseline(SkTextBaseline? value); external set fontSize(double? value); - - external List? get fontFamilies; + external set letterSpacing(double? value); + external set wordSpacing(double? value); + external set heightMultiplier(double? value); + external set locale(String? value); external set fontFamilies(List? value); + external set fontStyle(SkFontStyle? value); + external set shadows(List? value); + external set fontFeatures(List? value); +} - external SkFontStyle? get fontStyle; +@JS() +@anonymous +class SkStrutStyleProperties { + external set fontFamilies(List? value); external set fontStyle(SkFontStyle? value); + external set fontSize(double? value); + external set heightMultiplier(double? value); + external set leading(double? value); + external set strutEnabled(bool? value); + external set forceStrutHeight(bool? value); +} + +@JS() +@anonymous +class SkPlaceholderStyleProperties { + external set width(double? value); + external set height(double? value); + external set alignment(SkPlaceholderAlignment? value); + external set offset(double? value); + external set baseline(SkTextBaseline? value); } @JS() @anonymous class SkFontStyle { - external SkFontWeight? get weight; external set weight(SkFontWeight? value); - - external SkFontSlant? get slant; external set slant(SkFontSlant? value); } +@JS() +@anonymous +class SkTextShadow { + external set color(Float32List? value); + external set offset(Float32List? value); + external set blurRadius(double? value); +} + +@JS() +@anonymous +class SkFontFeature { + external set name(String? value); + external set value(int? value); +} + @JS() @anonymous class SkFontMgr { @@ -1591,12 +1631,13 @@ class SkParagraph { external double getMaxIntrinsicWidth(); external double getMinIntrinsicWidth(); external double getMaxWidth(); - external List getRectsForRange( + external List getRectsForRange( int start, int end, SkRectHeightStyle heightStyle, SkRectWidthStyle widthStyle, ); + external List getRectsForPlaceholders(); external SkTextPosition getGlyphPositionAtCoordinate( double x, double y, @@ -1649,7 +1690,8 @@ class TypefaceFontProviderNamespace { Timer? _skObjectCollector; List _skObjectDeleteQueue = []; -final SkObjectFinalizationRegistry skObjectFinalizationRegistry = SkObjectFinalizationRegistry(js.allowInterop((SkDeletable deletable) { +final SkObjectFinalizationRegistry skObjectFinalizationRegistry = + SkObjectFinalizationRegistry(js.allowInterop((SkDeletable deletable) { _skObjectDeleteQueue.add(deletable); _skObjectCollector ??= _scheduleSkObjectCollection(); })); @@ -1668,19 +1710,20 @@ final SkObjectFinalizationRegistry skObjectFinalizationRegistry = SkObjectFinali /// yielding to the graphics system to render the frame on the screen if there /// is a large number of objects to delete, causing jank. Timer _scheduleSkObjectCollection() => Timer(Duration.zero, () { - html.window.performance.mark('SkObject collection-start'); - final int length = _skObjectDeleteQueue.length; - for (int i = 0; i < length; i++) { - _skObjectDeleteQueue[i].delete(); - } - _skObjectDeleteQueue = []; - - // Null out the timer so we can schedule a new one next time objects are - // scheduled for deletion. - _skObjectCollector = null; - html.window.performance.mark('SkObject collection-end'); - html.window.performance.measure('SkObject collection', 'SkObject collection-start', 'SkObject collection-end'); -}); + html.window.performance.mark('SkObject collection-start'); + final int length = _skObjectDeleteQueue.length; + for (int i = 0; i < length; i++) { + _skObjectDeleteQueue[i].delete(); + } + _skObjectDeleteQueue = []; + + // Null out the timer so we can schedule a new one next time objects are + // scheduled for deletion. + _skObjectCollector = null; + html.window.performance.mark('SkObject collection-end'); + html.window.performance.measure('SkObject collection', + 'SkObject collection-start', 'SkObject collection-end'); + }); /// Any Skia object that has a `delete` method. @JS() @@ -1716,7 +1759,8 @@ class SkObjectFinalizationRegistry { external Object? get _finalizationRegistryConstructor; /// Whether the current browser supports `FinalizationRegistry`. -bool browserSupportsFinalizationRegistry = _finalizationRegistryConstructor != null; +bool browserSupportsFinalizationRegistry = + _finalizationRegistryConstructor != null; @JS() class SkData { @@ -1741,7 +1785,7 @@ class SkImageInfo { external int get height; external bool get isEmpty; external bool get isOpaque; - external SkRect get bounds; + external Float32List get bounds; external int get width; external SkImageInfo makeAlphaType(SkAlphaType alphaType); external SkImageInfo makeColorSpace(SkColorSpace colorSpace); diff --git a/lib/web_ui/lib/src/engine/canvaskit/initialization.dart b/lib/web_ui/lib/src/engine/canvaskit/initialization.dart index c1d91ad525925..1bfe7cf97aa69 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/initialization.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/initialization.dart @@ -20,7 +20,7 @@ const bool canvasKitForceCpuOnly = /// NPM, update this URL to `https://unpkg.com/canvaskit-wasm@0.34.0/bin/`. const String canvasKitBaseUrl = String.fromEnvironment( 'FLUTTER_WEB_CANVASKIT_URL', - defaultValue: 'https://unpkg.com/canvaskit-wasm@0.17.3/bin/', + defaultValue: 'https://unpkg.com/canvaskit-wasm@0.18.1/bin/', ); /// Initialize CanvasKit. diff --git a/lib/web_ui/lib/src/engine/canvaskit/painting.dart b/lib/web_ui/lib/src/engine/canvaskit/painting.dart index 550ff12351e40..276e6bb1ccfc3 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/painting.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/painting.dart @@ -227,6 +227,9 @@ class CkPaint extends ManagedSkiaObject implements ui.Paint { paint.setColorFilter(_ckColorFilter?.skiaObject); paint.setImageFilter(_imageFilter?.skiaObject); paint.setFilterQuality(toSkFilterQuality(_filterQuality)); + paint.setStrokeCap(toSkStrokeCap(_strokeCap)); + paint.setStrokeJoin(toSkStrokeJoin(_strokeJoin)); + paint.setStrokeMiter(_strokeMiterLimit); return paint; } diff --git a/lib/web_ui/lib/src/engine/canvaskit/path.dart b/lib/web_ui/lib/src/engine/canvaskit/path.dart index 86a758baf15fb..810c76e386a49 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/path.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/path.dart @@ -11,11 +11,15 @@ part of engine; class CkPath implements ui.Path { final SkPath _skPath; - CkPath() : _skPath = SkPath(), _fillType = ui.PathFillType.nonZero { + CkPath() + : _skPath = SkPath(), + _fillType = ui.PathFillType.nonZero { _skPath.setFillType(toSkFillType(_fillType)); } - CkPath.from(CkPath other) : _skPath = SkPath(other._skPath), _fillType = other.fillType { + CkPath.from(CkPath other) + : _skPath = SkPath(other._skPath), + _fillType = other.fillType { _skPath.setFillType(toSkFillType(_fillType)); } @@ -89,22 +93,10 @@ class CkPath implements ui.Path { @override void addRRect(ui.RRect rrect) { - final SkFloat32List skRadii = mallocFloat32List(8); - final Float32List radii = skRadii.toTypedArray(); - radii[0] = rrect.tlRadiusX; - radii[1] = rrect.tlRadiusY; - radii[2] = rrect.trRadiusX; - radii[3] = rrect.trRadiusY; - radii[4] = rrect.brRadiusX; - radii[5] = rrect.brRadiusY; - radii[6] = rrect.blRadiusX; - radii[7] = rrect.blRadiusY; - _skPath.addRoundRect( - toOuterSkRect(rrect), - radii, + _skPath.addRRect( + toSkRRect(rrect), false, ); - freeFloat32List(skRadii); } @override @@ -195,7 +187,7 @@ class CkPath implements ui.Path { } @override - ui.Rect getBounds() => _skPath.getBounds().toRect(); + ui.Rect getBounds() => fromSkRect(_skPath.getBounds()); @override void lineTo(double x, double y) { diff --git a/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart b/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart index fa6793d0f2297..126ffd97648de 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart @@ -13,7 +13,7 @@ class CkPictureRecorder implements ui.PictureRecorder { CkCanvas beginRecording(ui.Rect bounds) { _cullRect = bounds; final SkPictureRecorder recorder = _skRecorder = SkPictureRecorder(); - final SkRect skRect = toSkRect(bounds); + final Float32List skRect = toSkRect(bounds); final SkCanvas skCanvas = recorder.beginRecording(skRect); return _recordingCanvas = CkCanvas(skCanvas); } diff --git a/lib/web_ui/lib/src/engine/canvaskit/text.dart b/lib/web_ui/lib/src/engine/canvaskit/text.dart index 146a9f0baf4e4..a033f78f2e521 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/text.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/text.dart @@ -29,15 +29,23 @@ class CkParagraphStyle implements ui.ParagraphStyle { textHeightBehavior, fontWeight, fontStyle, + strutStyle, ellipsis, + locale, ) { _textDirection = textDirection ?? ui.TextDirection.ltr; _fontFamily = fontFamily; + _fontSize = fontSize; + _fontWeight = fontWeight; + _fontStyle = fontStyle; } SkParagraphStyle skParagraphStyle; ui.TextDirection? _textDirection; String? _fontFamily; + double? _fontSize; + ui.FontWeight? _fontWeight; + ui.FontStyle? _fontStyle; static SkTextStyleProperties toSkTextStyleProperties( String? fontFamily, @@ -63,6 +71,46 @@ class CkParagraphStyle implements ui.ParagraphStyle { return skTextStyle; } + static SkStrutStyleProperties toSkStrutStyleProperties(ui.StrutStyle value) { + EngineStrutStyle style = value as EngineStrutStyle; + final SkStrutStyleProperties skStrutStyle = SkStrutStyleProperties(); + if (style._fontFamily != null) { + final List fontFamilies = [style._fontFamily!]; + if (style._fontFamilyFallback != null) { + fontFamilies.addAll(style._fontFamilyFallback!); + } + skStrutStyle.fontFamilies = fontFamilies; + } else { + // If no strut font family is given, default to Roboto. + skStrutStyle.fontFamilies = ['Roboto']; + } + + if (style._fontSize != null) { + skStrutStyle.fontSize = style._fontSize; + } + + if (style._height != null) { + skStrutStyle.heightMultiplier = style._height; + } + + if (style._leading != null) { + skStrutStyle.leading = style._leading; + } + + if (style._fontWeight != null || style._fontStyle != null) { + skStrutStyle.fontStyle = + toSkFontStyle(style._fontWeight, style._fontStyle); + } + + if (style._forceStrutHeight != null) { + skStrutStyle.forceStrutHeight = style._forceStrutHeight; + } + + skStrutStyle.strutEnabled = true; + + return skStrutStyle; + } + static SkParagraphStyle toSkParagraphStyle( ui.TextAlign? textAlign, ui.TextDirection? textDirection, @@ -73,7 +121,9 @@ class CkParagraphStyle implements ui.ParagraphStyle { ui.TextHeightBehavior? textHeightBehavior, ui.FontWeight? fontWeight, ui.FontStyle? fontStyle, + ui.StrutStyle? strutStyle, String? ellipsis, + ui.Locale? locale, ) { final SkParagraphStyleProperties properties = SkParagraphStyleProperties(); @@ -85,6 +135,10 @@ class CkParagraphStyle implements ui.ParagraphStyle { properties.textDirection = toSkTextDirection(textDirection); } + if (maxLines != null) { + properties.maxLines = maxLines; + } + if (height != null) { properties.heightMultiplier = height; } @@ -93,25 +147,52 @@ class CkParagraphStyle implements ui.ParagraphStyle { properties.textHeightBehavior = textHeightBehavior.encode(); } - if (maxLines != null) { - properties.maxLines = maxLines; - } - if (ellipsis != null) { properties.ellipsis = ellipsis; } + if (strutStyle != null) { + properties.strutStyle = toSkStrutStyleProperties(strutStyle); + } + properties.textStyle = toSkTextStyleProperties(fontFamily, fontSize, fontWeight, fontStyle); return canvasKit.ParagraphStyle(properties); } + + CkTextStyle getTextStyle() { + return CkTextStyle( + fontFamily: _fontFamily, + fontSize: _fontSize, + fontWeight: _fontWeight, + fontStyle: _fontStyle, + ); + } } class CkTextStyle implements ui.TextStyle { SkTextStyle skTextStyle; + + ui.Color? color; + ui.TextDecoration? decoration; + ui.Color? decorationColor; + ui.TextDecorationStyle? decorationStyle; + double? decorationThickness; + ui.FontWeight? fontWeight; + ui.FontStyle? fontStyle; + ui.TextBaseline? textBaseline; + String? fontFamily; + List? fontFamilyFallback; + double? fontSize; + double? letterSpacing; + double? wordSpacing; + double? height; + ui.Locale? locale; CkPaint? background; CkPaint? foreground; + List? shadows; + List? fontFeatures; factory CkTextStyle({ ui.Color? color, @@ -162,10 +243,38 @@ class CkTextStyle implements ui.TextStyle { properties.decorationThickness = decorationThickness; } + if (decorationColor != null) { + properties.decorationColor = makeFreshSkColor(decorationColor); + } + + if (decorationStyle != null) { + properties.decorationStyle = toSkTextDecorationStyle(decorationStyle); + } + + if (textBaseline != null) { + properties.textBaseline = toSkTextBaseline(textBaseline); + } + if (fontSize != null) { properties.fontSize = fontSize; } + if (letterSpacing != null) { + properties.letterSpacing = letterSpacing; + } + + if (wordSpacing != null) { + properties.wordSpacing = wordSpacing; + } + + if (height != null) { + properties.heightMultiplier = height; + } + + if (locale != null) { + properties.locale = locale.toLanguageTag(); + } + if (fontFamily == null || !skiaFontCollection.registeredFamilies.contains(fontFamily)) { fontFamily = 'Roboto'; @@ -187,21 +296,103 @@ class CkTextStyle implements ui.TextStyle { properties.foregroundColor = makeFreshSkColor(foreground.color); } - // TODO(hterkelsen): Add support for - // - decorationColor - // - decorationStyle - // - textBaseline - // - letterSpacing - // - wordSpacing - // - height - // - locale - // - shadows - // - fontFeatures + if (shadows != null) { + List ckShadows = []; + for (ui.Shadow shadow in shadows) { + final ckShadow = SkTextShadow(); + ckShadow.color = makeFreshSkColor(shadow.color); + ckShadow.offset = toSkPoint(shadow.offset); + ckShadow.blurRadius = shadow.blurRadius; + ckShadows.add(ckShadow); + } + properties.shadows = ckShadows; + } + + if (fontFeatures != null) { + List ckFontFeatures = []; + for (ui.FontFeature fontFeature in fontFeatures) { + SkFontFeature ckFontFeature = SkFontFeature(); + ckFontFeature.name = fontFeature.feature; + ckFontFeature.value = fontFeature.value; + ckFontFeatures.add(ckFontFeature); + } + properties.fontFeatures = ckFontFeatures; + } + return CkTextStyle._( - canvasKit.TextStyle(properties), foreground, background); + canvasKit.TextStyle(properties), + color, + decoration, + decorationColor, + decorationStyle, + decorationThickness, + fontWeight, + fontStyle, + textBaseline, + fontFamily, + fontFamilyFallback, + fontSize, + letterSpacing, + wordSpacing, + height, + locale, + background, + foreground, + shadows, + fontFeatures, + ); + } + + /// Merges this text style with [other] and returns the new text style. + /// + /// The values in this text style are used unless [other] specifically + /// overrides it. + CkTextStyle mergeWith(CkTextStyle other) { + return CkTextStyle( + color: other.color ?? color, + decoration: other.decoration ?? decoration, + decorationColor: other.decorationColor ?? decorationColor, + decorationStyle: other.decorationStyle ?? decorationStyle, + decorationThickness: other.decorationThickness ?? decorationThickness, + fontWeight: other.fontWeight ?? fontWeight, + fontStyle: other.fontStyle ?? fontStyle, + textBaseline: other.textBaseline ?? textBaseline, + fontFamily: other.fontFamily ?? fontFamily, + fontFamilyFallback: other.fontFamilyFallback ?? fontFamilyFallback, + fontSize: other.fontSize ?? fontSize, + letterSpacing: other.letterSpacing ?? letterSpacing, + wordSpacing: other.wordSpacing ?? wordSpacing, + height: other.height ?? height, + locale: other.locale ?? locale, + background: other.background ?? background, + foreground: other.foreground ?? foreground, + shadows: other.shadows ?? shadows, + fontFeatures: other.fontFeatures ?? fontFeatures, + ); } - CkTextStyle._(this.skTextStyle, this.foreground, this.background); + CkTextStyle._( + this.skTextStyle, + this.color, + this.decoration, + this.decorationColor, + this.decorationStyle, + this.decorationThickness, + this.fontWeight, + this.fontStyle, + this.textBaseline, + this.fontFamily, + this.fontFamilyFallback, + this.fontSize, + this.letterSpacing, + this.wordSpacing, + this.height, + this.locale, + this.background, + this.foreground, + this.shadows, + this.fontFeatures, + ); } SkFontStyle toSkFontStyle(ui.FontWeight? fontWeight, ui.FontStyle? fontStyle) { @@ -260,6 +451,9 @@ class CkParagraph extends ManagedSkiaObject case _ParagraphCommandType.pushStyle: builder.pushStyle(command.style!); break; + case _ParagraphCommandType.addPlaceholder: + builder._addPlaceholder(command.placeholderStyle!); + break; } } @@ -304,10 +498,10 @@ class CkParagraph extends ManagedSkiaObject @override double get width => skiaObject.getMaxWidth(); - // TODO(hterkelsen): Implement placeholders once it's in CanvasKit @override List getBoxesForPlaceholders() { - return const []; + List> skRects = skiaObject.getRectsForPlaceholders(); + return skRectsToTextBoxes(skRects); } @override @@ -321,22 +515,26 @@ class CkParagraph extends ManagedSkiaObject return const []; } - List skRects = skiaObject.getRectsForRange( + List> skRects = skiaObject.getRectsForRange( start, end, toSkRectHeightStyle(boxHeightStyle), toSkRectWidthStyle(boxWidthStyle), ); + return skRectsToTextBoxes(skRects); + } + + List skRectsToTextBoxes(List> skRects) { List result = []; for (int i = 0; i < skRects.length; i++) { - final SkRect rect = skRects[i]; + final List rect = skRects[i]; result.add(ui.TextBox.fromLTRBD( - rect.fLeft, - rect.fTop, - rect.fRight, - rect.fBottom, + rect[0], + rect[1], + rect[2], + rect[3], _paragraphStyle._textDirection!, )); } @@ -404,16 +602,21 @@ class CkParagraphBuilder implements ui.ParagraphBuilder { final SkParagraphBuilder _paragraphBuilder; final CkParagraphStyle _style; final List<_ParagraphCommand> _commands; + int _placeholderCount; + final List _placeholderScales; + final List _styleStack; CkParagraphBuilder(ui.ParagraphStyle style) : _commands = <_ParagraphCommand>[], _style = style as CkParagraphStyle, + _placeholderCount = 0, + _placeholderScales = [], + _styleStack = [], _paragraphBuilder = canvasKit.ParagraphBuilder.MakeFromFontProvider( style.skParagraphStyle, skiaFontCollection.fontProvider, ); - // TODO(hterkelsen): Implement placeholders. @override void addPlaceholder( double width, @@ -423,7 +626,44 @@ class CkParagraphBuilder implements ui.ParagraphBuilder { double? baselineOffset, ui.TextBaseline? baseline, }) { - throw UnimplementedError('addPlaceholder'); + // Require a baseline to be specified if using a baseline-based alignment. + assert((alignment == ui.PlaceholderAlignment.aboveBaseline || + alignment == ui.PlaceholderAlignment.belowBaseline || + alignment == ui.PlaceholderAlignment.baseline) + ? baseline != null + : true); + + _placeholderCount++; + _placeholderScales.add(scale); + SkPlaceholderStyleProperties placeholderStyle = toSkPlaceholderStyle( + width * scale, + height * scale, + alignment, + (baselineOffset ?? height) * scale, + baseline ?? ui.TextBaseline.alphabetic, + ); + _addPlaceholder(placeholderStyle); + } + + void _addPlaceholder(SkPlaceholderStyleProperties placeholderStyle) { + _commands.add(_ParagraphCommand.addPlaceholder(placeholderStyle)); + _paragraphBuilder.addPlaceholder(placeholderStyle); + } + + static SkPlaceholderStyleProperties toSkPlaceholderStyle( + double width, + double height, + ui.PlaceholderAlignment alignment, + double baselineOffset, + ui.TextBaseline baseline, + ) { + final properties = SkPlaceholderStyleProperties(); + properties.width = width; + properties.height = height; + properties.alignment = toSkPlaceholderAlignment(alignment); + properties.offset = baselineOffset; + properties.baseline = toSkTextBaseline(baseline); + return properties; } @override @@ -446,22 +686,28 @@ class CkParagraphBuilder implements ui.ParagraphBuilder { } @override - int get placeholderCount => throw UnimplementedError('placeholderCount'); + int get placeholderCount => _placeholderCount; - // TODO(hterkelsen): Implement this once CanvasKit exposes placeholders. @override - List get placeholderScales => const []; + List get placeholderScales => _placeholderScales; @override void pop() { _commands.add(const _ParagraphCommand.pop()); + _styleStack.removeLast(); _paragraphBuilder.pop(); } + CkTextStyle _peekStyle() => + _styleStack.isEmpty ? _style.getTextStyle() : _styleStack.last; + @override void pushStyle(ui.TextStyle style) { - final CkTextStyle skStyle = style as CkTextStyle; - _commands.add(_ParagraphCommand.pushStyle(skStyle)); + final CkTextStyle baseStyle = _peekStyle(); + final CkTextStyle ckStyle = style as CkTextStyle; + final CkTextStyle skStyle = baseStyle.mergeWith(ckStyle); + _styleStack.add(skStyle); + _commands.add(_ParagraphCommand.pushStyle(ckStyle)); if (skStyle.foreground != null || skStyle.background != null) { final SkPaint foreground = skStyle.foreground?.skiaObject ?? SkPaint(); final SkPaint background = skStyle.background?.skiaObject ?? SkPaint(); @@ -477,20 +723,33 @@ class _ParagraphCommand { final _ParagraphCommandType type; final String? text; final CkTextStyle? style; + final SkPlaceholderStyleProperties? placeholderStyle; - const _ParagraphCommand._(this.type, this.text, this.style); + const _ParagraphCommand._( + this.type, + this.text, + this.style, + this.placeholderStyle, + ); const _ParagraphCommand.addText(String text) - : this._(_ParagraphCommandType.addText, text, null); + : this._(_ParagraphCommandType.addText, text, null, null); - const _ParagraphCommand.pop() : this._(_ParagraphCommandType.pop, null, null); + const _ParagraphCommand.pop() + : this._(_ParagraphCommandType.pop, null, null, null); const _ParagraphCommand.pushStyle(CkTextStyle style) - : this._(_ParagraphCommandType.pushStyle, null, style); + : this._(_ParagraphCommandType.pushStyle, null, style, null); + + const _ParagraphCommand.addPlaceholder( + SkPlaceholderStyleProperties placeholderStyle) + : this._( + _ParagraphCommandType.addPlaceholder, null, null, placeholderStyle); } enum _ParagraphCommandType { addText, pop, pushStyle, + addPlaceholder, } diff --git a/lib/web_ui/lib/src/engine/dom_canvas.dart b/lib/web_ui/lib/src/engine/dom_canvas.dart index 0eabf6d8a4aac..c6d32a02e8f70 100644 --- a/lib/web_ui/lib/src/engine/dom_canvas.dart +++ b/lib/web_ui/lib/src/engine/dom_canvas.dart @@ -28,7 +28,7 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking { } @override - void clipRect(ui.Rect rect) { + void clipRect(ui.Rect rect, ui.ClipOp op) { throw UnimplementedError(); } @@ -68,75 +68,16 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking { @override void drawRect(ui.Rect rect, SurfacePaintData paint) { - _drawRect(rect, paint, 'draw-rect'); - } - - html.Element _drawRect(ui.Rect rect, SurfacePaintData paint, String tagName) { - assert(paint.shader == null); - final html.Element rectangle = html.Element.tag(tagName); - assert(() { - rectangle.setAttribute('flt-rect', '$rect'); - rectangle.setAttribute('flt-paint', '$paint'); - return true; - }()); - String effectiveTransform; - final bool isStroke = paint.style == ui.PaintingStyle.stroke; - final double strokeWidth = paint.strokeWidth ?? 0.0; - final double left = math.min(rect.left, rect.right); - final double right = math.max(rect.left, rect.right); - final double top = math.min(rect.top, rect.bottom); - final double bottom = math.max(rect.top, rect.bottom); - if (currentTransform.isIdentity()) { - if (isStroke) { - effectiveTransform = - 'translate(${left - (strokeWidth / 2.0)}px, ${top - (strokeWidth / 2.0)}px)'; - } else { - effectiveTransform = 'translate(${left}px, ${top}px)'; - } - } else { - // Clone to avoid mutating _transform. - final Matrix4 translated = currentTransform.clone(); - if (isStroke) { - translated.translate( - left - (strokeWidth / 2.0), top - (strokeWidth / 2.0)); - } else { - translated.translate(left, top); - } - effectiveTransform = matrix4ToCssTransform(translated); - } - final html.CssStyleDeclaration style = rectangle.style; - style - ..position = 'absolute' - ..transformOrigin = '0 0 0' - ..transform = effectiveTransform; - - final String cssColor = - paint.color == null ? '#000000' : colorToCssString(paint.color)!; - - if (paint.maskFilter != null) { - style.filter = 'blur(${paint.maskFilter!.webOnlySigma}px)'; - } - - if (isStroke) { - style - ..width = '${right - left - strokeWidth}px' - ..height = '${bottom - top - strokeWidth}px' - ..border = '${strokeWidth}px solid $cssColor'; - } else { - style - ..width = '${right - left}px' - ..height = '${bottom - top}px' - ..backgroundColor = cssColor; - } - - currentElement.append(rectangle); - return rectangle; + currentElement.append(_buildDrawRectElement(rect, paint, 'draw-rect', + currentTransform)); } @override void drawRRect(ui.RRect rrect, SurfacePaintData paint) { - html.Element element = _drawRect(rrect.outerRect, paint, 'draw-rrect'); - element.style.borderRadius = '${rrect.blRadiusX.toStringAsFixed(3)}px'; + html.Element element = _buildDrawRectElement(rrect.outerRect, + paint, 'draw-rrect', currentTransform); + _applyRRectBorderRadius(element.style, rrect); + currentElement.append(element); } @override @@ -199,3 +140,108 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking { // No reuse of elements yet to handle here. Noop. } } + +html.HtmlElement _buildDrawRectElement(ui.Rect rect, SurfacePaintData paint, String tagName, + Matrix4 transform) { + assert(paint.shader == null); + final html.HtmlElement rectangle = html.Element.tag(tagName) as html.HtmlElement; + assert(() { + rectangle.setAttribute('flt-rect', '$rect'); + rectangle.setAttribute('flt-paint', '$paint'); + return true; + }()); + String effectiveTransform; + final bool isStroke = paint.style == ui.PaintingStyle.stroke; + final double strokeWidth = paint.strokeWidth ?? 0.0; + final double left = math.min(rect.left, rect.right); + final double right = math.max(rect.left, rect.right); + final double top = math.min(rect.top, rect.bottom); + final double bottom = math.max(rect.top, rect.bottom); + if (transform.isIdentity()) { + if (isStroke) { + effectiveTransform = + 'translate(${left - (strokeWidth / 2.0)}px, ${top - (strokeWidth / 2.0)}px)'; + } else { + effectiveTransform = 'translate(${left}px, ${top}px)'; + } + } else { + // Clone to avoid mutating _transform. + final Matrix4 translated = transform.clone(); + if (isStroke) { + translated.translate( + left - (strokeWidth / 2.0), top - (strokeWidth / 2.0)); + } else { + translated.translate(left, top); + } + effectiveTransform = matrix4ToCssTransform(translated); + } + final html.CssStyleDeclaration style = rectangle.style; + style + ..position = 'absolute' + ..transformOrigin = '0 0 0' + ..transform = effectiveTransform; + + final String cssColor = + paint.color == null ? '#000000' : colorToCssString(paint.color)!; + + if (paint.maskFilter != null) { + style.filter = 'blur(${paint.maskFilter!.webOnlySigma}px)'; + } + + if (isStroke) { + style + ..width = '${right - left - strokeWidth}px' + ..height = '${bottom - top - strokeWidth}px' + ..border = '${strokeWidth}px solid $cssColor'; + } else { + style + ..width = '${right - left}px' + ..height = '${bottom - top}px' + ..backgroundColor = cssColor; + } + return rectangle; +} + +void _applyRRectBorderRadius(html.CssStyleDeclaration style, ui.RRect rrect) { + if (rrect.tlRadiusX == rrect.trRadiusX && + rrect.tlRadiusX == rrect.blRadiusX && + rrect.tlRadiusX == rrect.brRadiusX && + rrect.tlRadiusX == rrect.tlRadiusY && + rrect.trRadiusX == rrect.trRadiusY && + rrect.blRadiusX == rrect.blRadiusY && + rrect.brRadiusX == rrect.brRadiusY) { + style.borderRadius = '${rrect.blRadiusX.toStringAsFixed(3)}px'; + return; + } + // Non-uniform. Apply each corner radius. + style.borderTopLeftRadius = '${rrect.tlRadiusX.toStringAsFixed(3)}px ' + '${rrect.tlRadiusY.toStringAsFixed(3)}px'; + style.borderTopRightRadius = '${rrect.trRadiusX.toStringAsFixed(3)}px ' + '${rrect.trRadiusY.toStringAsFixed(3)}px'; + style.borderBottomLeftRadius = '${rrect.blRadiusX.toStringAsFixed(3)}px ' + '${rrect.blRadiusY.toStringAsFixed(3)}px'; + style.borderBottomRightRadius = '${rrect.brRadiusX.toStringAsFixed(3)}px ' + '${rrect.brRadiusY.toStringAsFixed(3)}px'; +} + +html.Element _pathToSvgElement(SurfacePath path, SurfacePaintData paint, + String width, String height) { + final StringBuffer sb = StringBuffer(); + sb.write( + ''); + sb.write(''); + sb.write(''); + return html.Element.html(sb.toString(), treeSanitizer: _NullTreeSanitizer()); +} diff --git a/lib/web_ui/lib/src/engine/dom_renderer.dart b/lib/web_ui/lib/src/engine/dom_renderer.dart index 9e1a3b50ff375..415ea66c86190 100644 --- a/lib/web_ui/lib/src/engine/dom_renderer.dart +++ b/lib/web_ui/lib/src/engine/dom_renderer.dart @@ -307,6 +307,21 @@ flt-glass-pane * { ''', sheet.cssRules.length); } + // This css prevents an autofill overlay brought by the browser during + // text field autofill by delaying the transition effect. + // See: https://github.com/flutter/flutter/issues/61132. + if(browserHasAutofillOverlay()) { + sheet.insertRule(''' +.transparentTextEditing:-webkit-autofill, +.transparentTextEditing:-webkit-autofill:hover, +.transparentTextEditing:-webkit-autofill:focus, +.transparentTextEditing:-webkit-autofill:active { + -webkit-transition-delay: 99999s; +} +''', sheet.cssRules.length); + } + + final html.BodyElement bodyElement = html.document.body!; setElementStyle(bodyElement, 'position', 'fixed'); setElementStyle(bodyElement, 'top', '0'); diff --git a/lib/web_ui/lib/src/engine/engine_canvas.dart b/lib/web_ui/lib/src/engine/engine_canvas.dart index 5b0f7d31d8167..c197515900a67 100644 --- a/lib/web_ui/lib/src/engine/engine_canvas.dart +++ b/lib/web_ui/lib/src/engine/engine_canvas.dart @@ -33,7 +33,7 @@ abstract class EngineCanvas { void transform(Float32List matrix4); - void clipRect(ui.Rect rect); + void clipRect(ui.Rect rect, ui.ClipOp clipOp); void clipRRect(ui.RRect rrect); @@ -222,7 +222,7 @@ mixin SaveStackTracking on EngineCanvas { /// /// Classes that override this method must call `super.clipRect()`. @override - void clipRect(ui.Rect rect) { + void clipRect(ui.Rect rect, ui.ClipOp op) { _clipStack ??= <_SaveClipEntry>[]; _clipStack!.add(_SaveClipEntry.rect(rect, _currentTransform.clone())); } diff --git a/lib/web_ui/lib/src/engine/html/clip.dart b/lib/web_ui/lib/src/engine/html/clip.dart index 0cab237946277..bc8cbc456b5ed 100644 --- a/lib/web_ui/lib/src/engine/html/clip.dart +++ b/lib/web_ui/lib/src/engine/html/clip.dart @@ -25,18 +25,6 @@ mixin _DomClip on PersistedContainerSurface { @override html.Element createElement() { final html.Element element = defaultCreateElement('flt-clip'); - if (!debugShowClipLayers) { - // Hide overflow in production mode. When debugging we want to see the - // clipped picture in full. - element.style - ..overflow = 'hidden' - ..zIndex = '0'; - } else { - // Display the outline of the clipping region. When debugShowClipLayers is - // `true` we don't hide clip overflow (see above). This outline helps - // visualizing clip areas. - element.style.boxShadow = 'inset 0 0 10px green'; - } _childContainer = html.Element.tag('flt-clip-interior'); if (_debugExplainSurfaceStats) { // This creates an additional interior element. Count it too. @@ -57,14 +45,32 @@ mixin _DomClip on PersistedContainerSurface { // together. _childContainer = null; } + + void applyOverflow(html.Element element, ui.Clip? clipBehaviour) { + if (!debugShowClipLayers) { + // Hide overflow in production mode. When debugging we want to see the + // clipped picture in full. + if (clipBehaviour != ui.Clip.none) { + element.style + ..overflow = 'hidden' + ..zIndex = '0'; + } + } else { + // Display the outline of the clipping region. When debugShowClipLayers is + // `true` we don't hide clip overflow (see above). This outline helps + // visualizing clip areas. + element.style.boxShadow = 'inset 0 0 10px green'; + } + } } /// A surface that creates a rectangular clip. class PersistedClipRect extends PersistedContainerSurface with _DomClip implements ui.ClipRectEngineLayer { - PersistedClipRect(PersistedClipRect? oldLayer, this.rect) : super(oldLayer); - + PersistedClipRect(PersistedClipRect? oldLayer, this.rect, this.clipBehavior) + : super(oldLayer); + final ui.Clip? clipBehavior; final ui.Rect rect; @override @@ -87,6 +93,7 @@ class PersistedClipRect extends PersistedContainerSurface ..top = '${rect.top}px' ..width = '${rect.right - rect.left}px' ..height = '${rect.bottom - rect.top}px'; + applyOverflow(rootElement!, clipBehavior); // Translate the child container in the opposite direction to compensate for // the shift in the coordinate system introduced by the translation of the @@ -99,7 +106,7 @@ class PersistedClipRect extends PersistedContainerSurface @override void update(PersistedClipRect oldSurface) { super.update(oldSurface); - if (rect != oldSurface.rect) { + if (rect != oldSurface.rect || clipBehavior != oldSurface.clipBehavior) { apply(); } } @@ -134,7 +141,8 @@ class PersistedClipRRect extends PersistedContainerSurface @override void apply() { - rootElement!.style + html.CssStyleDeclaration style = rootElement!.style; + style ..left = '${rrect.left}px' ..top = '${rrect.top}px' ..width = '${rrect.width}px' @@ -143,6 +151,7 @@ class PersistedClipRRect extends PersistedContainerSurface ..borderTopRightRadius = '${rrect.trRadiusX}px' ..borderBottomRightRadius = '${rrect.brRadiusX}px' ..borderBottomLeftRadius = '${rrect.blRadiusX}px'; + applyOverflow(rootElement!, clipBehavior); // Translate the child container in the opposite direction to compensate for // the shift in the coordinate system introduced by the translation of the @@ -155,7 +164,7 @@ class PersistedClipRRect extends PersistedContainerSurface @override void update(PersistedClipRRect oldSurface) { super.update(oldSurface); - if (rrect != oldSurface.rrect) { + if (rrect != oldSurface.rrect || clipBehavior != oldSurface.clipBehavior) { apply(); } } diff --git a/lib/web_ui/lib/src/engine/html/recording_canvas.dart b/lib/web_ui/lib/src/engine/html/recording_canvas.dart index cb6a1ed626fed..9f7e6b93bca92 100644 --- a/lib/web_ui/lib/src/engine/html/recording_canvas.dart +++ b/lib/web_ui/lib/src/engine/html/recording_canvas.dart @@ -275,7 +275,7 @@ class RecordingCanvas { void clipRect(ui.Rect rect, ui.ClipOp clipOp) { assert(!_recordingEnded); - final PaintClipRect command = PaintClipRect(rect, clipOp); + final DrawCommand command = PaintClipRect(rect, clipOp); switch (clipOp) { case ui.ClipOp.intersect: _paintBounds.clipRect(rect, command); @@ -810,7 +810,7 @@ class PaintClipRect extends DrawCommand { @override void apply(EngineCanvas canvas) { - canvas.clipRect(rect); + canvas.clipRect(rect, clipOp); } @override diff --git a/lib/web_ui/lib/src/engine/html/render_vertices.dart b/lib/web_ui/lib/src/engine/html/render_vertices.dart index 11d400bfc5f5b..c0d564bca1e76 100644 --- a/lib/web_ui/lib/src/engine/html/render_vertices.dart +++ b/lib/web_ui/lib/src/engine/html/render_vertices.dart @@ -55,7 +55,7 @@ void initWebGl() { } void disposeWebGl() { - _OffscreenCanvas.dispose(); + _GlContextCache.dispose(); _glRenderer = null; } @@ -69,6 +69,9 @@ abstract class _GlRenderer { ui.BlendMode blendMode, SurfacePaintData paint); + Object? drawRect(ui.Rect targetRect, _GlContext gl, _GlProgram glProgram, + NormalizedGradient gradient, int widthInPixels, int heightInPixels); + void drawHairline(html.CanvasRenderingContext2D? _ctx, Float32List positions); } @@ -77,51 +80,9 @@ abstract class _GlRenderer { /// This class gets instantiated on demand by Vertices constructor. For apps /// that don't use Vertices WebGlRenderer will be removed from release binary. class _WebGlRenderer implements _GlRenderer { - // Vertex shader transforms pixel space [Vertices.positions] to - // final clipSpace -1..1 coordinates with inverted Y Axis. - static const _vertexShaderTriangle = ''' - #version 300 es - layout (location=0) in vec4 position; - layout (location=1) in vec4 color; - uniform mat4 u_ctransform; - uniform vec4 u_scale; - uniform vec4 u_shift; - out vec4 vColor; - void main() { - gl_Position = ((u_ctransform * position) * u_scale) + u_shift; - vColor = color.zyxw; - }'''; - // This fragment shader enables Int32List of colors to be passed directly - // to gl context buffer for rendering by decoding RGBA8888. - static const _fragmentShaderTriangle = ''' - #version 300 es - precision highp float; - in vec4 vColor; - out vec4 fragColor; - void main() { - fragColor = vColor; - }'''; - - // WebGL 1 version of shaders above for compatibility with Safari. - static const _vertexShaderTriangleEs1 = ''' - attribute vec4 position; - attribute vec4 color; - uniform mat4 u_ctransform; - uniform vec4 u_scale; - uniform vec4 u_shift; - varying vec4 vColor; - void main() { - gl_Position = ((u_ctransform * position) * u_scale) + u_shift; - vColor = color.zyxw; - }'''; - // WebGL 1 version of shaders above for compatibility with Safari. - static const _fragmentShaderTriangleEs1 = ''' - precision highp float; - varying vec4 vColor; - void main() { - gl_FragColor = vColor; - }'''; + /// Cached vertex shader reused by [drawVertices] and gradients. + static String? _baseVertexShader; @override void drawVertices( html.CanvasRenderingContext2D? context, @@ -161,24 +122,24 @@ class _WebGlRenderer implements _GlRenderer { if (widthInPixels == 0 || heightInPixels == 0) { return; } - _GlContext gl = - _OffscreenCanvas.createGlContext(widthInPixels, heightInPixels)!; - _GlProgram glProgram = webGLVersion == 1 - ? gl.useAndCacheProgram( - _vertexShaderTriangleEs1, _fragmentShaderTriangleEs1)! - : gl.useAndCacheProgram( - _vertexShaderTriangle, _fragmentShaderTriangle)!; - - Object? transformUniform = gl.getUniformLocation(glProgram.program, 'u_ctransform'); - Matrix4 transformAtOffset = transform.clone()..translate(-offsetX, -offsetY); + final String vertexShader = writeBaseVertexShader(); + final String fragmentShader = _writeVerticesFragmentShader(); + _GlContext gl = _GlContextCache.createGlContext(widthInPixels, heightInPixels)!; + + _GlProgram glProgram = gl.useAndCacheProgram(vertexShader, fragmentShader)!; + + Object transformUniform = gl.getUniformLocation(glProgram.program, + 'u_ctransform'); + Matrix4 transformAtOffset = transform.clone() + ..translate(-offsetX, -offsetY); gl.setUniformMatrix4fv(transformUniform, false, transformAtOffset.storage); // Set uniform to scale 0..width/height pixels coordinates to -1..1 // clipspace range and flip the Y axis. - Object? resolution = gl.getUniformLocation(glProgram.program, 'u_scale'); + Object resolution = gl.getUniformLocation(glProgram.program, 'u_scale'); gl.setUniform4f(resolution, 2.0 / widthInPixels.toDouble(), -2.0 / heightInPixels.toDouble(), 1, 1); - Object? shift = gl.getUniformLocation(glProgram.program, 'u_shift'); + Object shift = gl.getUniformLocation(glProgram.program, 'u_shift'); gl.setUniform4f(shift, -1, 1, 0, 0); // Setup geometry. @@ -186,8 +147,11 @@ class _WebGlRenderer implements _GlRenderer { assert(positionsBuffer != null); // ignore: unnecessary_null_comparison gl.bindArrayBuffer(positionsBuffer); gl.bufferData(positions, gl.kStaticDraw); + Object? positionLoc = gl.getAttributeLocation(glProgram.program, 'position'); js_util.callMethod( - gl.glContext!, 'vertexAttribPointer', [0, 2, gl.kFloat, false, 0, 0]); + gl.glContext, 'vertexAttribPointer', [ + positionLoc, 2, gl.kFloat, false, 0, 0, + ]); gl.enableVertexAttribArray(0); // Setup color buffer. @@ -195,9 +159,9 @@ class _WebGlRenderer implements _GlRenderer { gl.bindArrayBuffer(colorsBuffer); // Buffer kBGRA_8888. gl.bufferData(vertices._colors, gl.kStaticDraw); - - js_util.callMethod(gl.glContext!, 'vertexAttribPointer', - [1, 4, gl.kUnsignedByte, true, 0, 0]); + Object colorLoc = gl.getAttributeLocation(glProgram.program, 'color'); + js_util.callMethod(gl.glContext, 'vertexAttribPointer', + [colorLoc, 4, gl.kUnsignedByte, true, 0, 0]); gl.enableVertexAttribArray(1); gl.clear(); final int vertexCount = positions.length ~/ 2; @@ -209,6 +173,138 @@ class _WebGlRenderer implements _GlRenderer { context.restore(); } + static final Uint16List _vertexIndicesForRect = Uint16List.fromList( + [ + 0, 1, 2, 2, 3, 0 + ] + ); + + /// Renders a rectangle using given program into an image resource. + /// + /// Browsers that support OffscreenCanvas and the transferToImageBitmap api + /// will return ImageBitmap, otherwise will return CanvasElement. + Object? drawRect(ui.Rect targetRect, _GlContext gl, _GlProgram glProgram, + NormalizedGradient gradient, int widthInPixels, int heightInPixels) { + // Setup rectangle coordinates. + final double left = targetRect.left; + final double top = targetRect.top; + final double right = targetRect.right; + final double bottom = targetRect.bottom; + // Form 2 triangles for rectangle. + final Float32List vertices = Float32List(8); + vertices[0] = left; + vertices[1] = top; + vertices[2] = right; + vertices[3] = top; + vertices[4] = right; + vertices[5] = bottom; + vertices[6] = left; + vertices[7] = bottom; + + Object transformUniform = gl.getUniformLocation( + glProgram.program, 'u_ctransform'); + gl.setUniformMatrix4fv(transformUniform, false, Matrix4.identity().storage); + + // Set uniform to scale 0..width/height pixels coordinates to -1..1 + // clipspace range and flip the Y axis. + Object resolution = gl.getUniformLocation(glProgram.program, 'u_scale'); + gl.setUniform4f(resolution, 2.0 / widthInPixels.toDouble(), + -2.0 / heightInPixels.toDouble(), 1, 1); + Object shift = gl.getUniformLocation(glProgram.program, 'u_shift'); + gl.setUniform4f(shift, -1, 1, 0, 0); + + // Setup geometry. + Object positionsBuffer = gl.createBuffer()!; + assert(positionsBuffer != null); // ignore: unnecessary_null_comparison + gl.bindArrayBuffer(positionsBuffer); + gl.bufferData(vertices, gl.kStaticDraw); + // Point an attribute to the currently bound vertex buffer object. + js_util.callMethod( + gl.glContext, 'vertexAttribPointer', + [0, 2, gl.kFloat, false, 0, 0]); + gl.enableVertexAttribArray(0); + + // Setup color buffer. + Object? colorsBuffer = gl.createBuffer(); + gl.bindArrayBuffer(colorsBuffer); + // Buffer kBGRA_8888. + final Int32List colors = Int32List.fromList([ + 0xFF00FF00, 0xFF0000FF, 0xFFFFFF00, 0xFF00FFFF, + ]); + gl.bufferData(colors, gl.kStaticDraw); + js_util.callMethod(gl.glContext, 'vertexAttribPointer', + [1, 4, gl.kUnsignedByte, true, 0, 0]); + gl.enableVertexAttribArray(1); + + Object? indexBuffer = gl.createBuffer(); + gl.bindElementArrayBuffer(indexBuffer); + gl.bufferElementData(_vertexIndicesForRect, gl.kStaticDraw); + + Object uRes = gl.getUniformLocation(glProgram.program, 'u_resolution'); + gl.setUniform2f(uRes, widthInPixels.toDouble(), heightInPixels.toDouble()); + + gl.clear(); + gl.viewport(0, 0, widthInPixels.toDouble(), heightInPixels.toDouble()); + + gl.drawElements(gl.kTriangles, _vertexIndicesForRect.length, gl.kUnsignedShort); + + Object? image = gl.readPatternData(); + + gl.bindArrayBuffer(null); + gl.bindElementArrayBuffer(null); + + return image; + } + + /// Creates a vertex shader transforms pixel space [Vertices.positions] to + /// final clipSpace -1..1 coordinates with inverted Y Axis. + /// #version 300 es + /// layout (location=0) in vec4 position; + /// layout (location=1) in vec4 color; + /// uniform mat4 u_ctransform; + /// uniform vec4 u_scale; + /// uniform vec4 u_shift; + /// out vec4 vColor; + /// void main() { + /// gl_Position = ((u_ctransform * position) * u_scale) + u_shift; + /// v_color = color.zyxw; + /// } + static String writeBaseVertexShader() { + if (_baseVertexShader == null) { + ShaderBuilder builder = ShaderBuilder(webGLVersion); + builder.addIn(ShaderType.kVec4, name: 'position'); + builder.addIn(ShaderType.kVec4, name: 'color'); + builder.addUniform(ShaderType.kMat4, name: 'u_ctransform'); + builder.addUniform(ShaderType.kVec4, name: 'u_scale'); + builder.addUniform(ShaderType.kVec4, name: 'u_shift'); + builder.addOut(ShaderType.kVec4, name: 'v_color'); + ShaderMethod method = builder.addMethod('main'); + method.addStatement( + 'gl_Position = ((u_ctransform * position) * u_scale) + u_shift;'); + method.addStatement('v_color = color.zyxw;'); + _baseVertexShader = builder.build(); + } + return _baseVertexShader!; + } + + /// This fragment shader enables Int32List of colors to be passed directly + /// to gl context buffer for rendering by decoding RGBA8888. + /// #version 300 es + /// precision mediump float; + /// in vec4 vColor; + /// out vec4 fragColor; + /// void main() { + /// fragColor = vColor; + /// } + String _writeVerticesFragmentShader() { + ShaderBuilder builder = ShaderBuilder.fragment(webGLVersion); + builder.floatPrecision = ShaderPrecision.kMedium; + builder.addIn(ShaderType.kVec4, name:'v_color'); + ShaderMethod method = builder.addMethod('main'); + method.addStatement('${builder.fragmentColor.name} = v_color;'); + return builder.build(); + } + @override void drawHairline(html.CanvasRenderingContext2D? _ctx, Float32List positions) { assert(positions != null); // ignore: unnecessary_null_comparison @@ -336,30 +432,33 @@ Float32List _convertVertexPositions(ui.VertexMode mode, Float32List positions) { /// Compiled and cached gl program. class _GlProgram { - final Object? program; + final Object program; _GlProgram(this.program); } /// JS Interop helper for webgl apis. class _GlContext { - final Object? glContext; + final Object glContext; final bool isOffscreen; dynamic _kCompileStatus; dynamic _kArrayBuffer; + dynamic _kElementArrayBuffer; dynamic _kStaticDraw; dynamic _kFloat; dynamic _kColorBufferBit; dynamic _kTriangles; dynamic _kLinkStatus; dynamic _kUnsignedByte; + dynamic _kUnsignedShort; dynamic _kRGBA; + Object? _canvas; int? _widthInPixels; int? _heightInPixels; static late Map _programCache; _GlContext.fromOffscreenCanvas(html.OffscreenCanvas canvas) - : glContext = canvas.getContext('webgl2', {'premultipliedAlpha': false}), + : glContext = canvas.getContext('webgl2', {'premultipliedAlpha': false})!, isOffscreen = true { _programCache = {}; _canvas = canvas; @@ -367,7 +466,7 @@ class _GlContext { _GlContext.fromCanvas(html.CanvasElement canvas, bool useWebGl1) : glContext = canvas.getContext(useWebGl1 ? 'webgl' : 'webgl2', - {'premultipliedAlpha': false}), + {'premultipliedAlpha': false})!, isOffscreen = false { _programCache = {}; _canvas = canvas; @@ -385,7 +484,7 @@ class _GlContext { // source/destination to draw part of the image data. js_util.callMethod(context, 'drawImage', [_canvas, 0, 0, _widthInPixels, _heightInPixels, - left, top, _widthInPixels, _heightInPixels]); + left, top, _widthInPixels, _heightInPixels]); } _GlProgram? useAndCacheProgram( @@ -396,9 +495,9 @@ class _GlContext { // Create and compile shaders. Object vertexShader = compileShader('VERTEX_SHADER', vertexShaderSource); Object fragmentShader = - compileShader('FRAGMENT_SHADER', fragmentShaderSource); + compileShader('FRAGMENT_SHADER', fragmentShaderSource); // Create a gl program and link shaders. - Object? program = createProgram(); + Object program = createProgram(); attachShader(program, vertexShader); attachShader(program, fragmentShader); linkProgram(program); @@ -414,57 +513,65 @@ class _GlContext { if (shader == null) { throw Exception(error); } - js_util.callMethod(glContext!, 'shaderSource', [shader, source]); - js_util.callMethod(glContext!, 'compileShader', [shader]); + js_util.callMethod(glContext, 'shaderSource', [shader, source]); + js_util.callMethod(glContext, 'compileShader', [shader]); bool shaderStatus = js_util - .callMethod(glContext!, 'getShaderParameter', [shader, compileStatus]); + .callMethod(glContext, 'getShaderParameter', [shader, compileStatus]); if (!shaderStatus) { throw Exception('Shader compilation failed: ${getShaderInfoLog(shader)}'); } return shader; } - Object? createProgram() => - js_util.callMethod(glContext!, 'createProgram', const []); + Object createProgram() => + js_util.callMethod(glContext, 'createProgram', const [])!; void attachShader(Object? program, Object shader) { - js_util.callMethod(glContext!, 'attachShader', [program, shader]); + js_util.callMethod(glContext, 'attachShader', [program, shader]); } - void linkProgram(Object? program) { - js_util.callMethod(glContext!, 'linkProgram', [program]); + void linkProgram(Object program) { + js_util.callMethod(glContext, 'linkProgram', [program]); if (!js_util - .callMethod(glContext!, 'getProgramParameter', [program, kLinkStatus])) { + .callMethod(glContext, 'getProgramParameter', [program, kLinkStatus])) { throw Exception(getProgramInfoLog(program)); } } void useProgram(Object? program) { - js_util.callMethod(glContext!, 'useProgram', [program]); + js_util.callMethod(glContext, 'useProgram', [program]); } Object? createBuffer() => - js_util.callMethod(glContext!, 'createBuffer', const []); + js_util.callMethod(glContext, 'createBuffer', const []); void bindArrayBuffer(Object? buffer) { - js_util.callMethod(glContext!, 'bindBuffer', [kArrayBuffer, buffer]); + js_util.callMethod(glContext, 'bindBuffer', [kArrayBuffer, buffer]); + } + + void bindElementArrayBuffer(Object? buffer) { + js_util.callMethod(glContext, 'bindBuffer', [kElementArrayBuffer, buffer]); } void deleteBuffer(Object buffer) { - js_util.callMethod(glContext!, 'deleteBuffer', [buffer]); + js_util.callMethod(glContext, 'deleteBuffer', [buffer]); } void bufferData(TypedData? data, dynamic type) { - js_util.callMethod(glContext!, 'bufferData', [kArrayBuffer, data, type]); + js_util.callMethod(glContext, 'bufferData', [kArrayBuffer, data, type]); + } + + void bufferElementData(TypedData? data, dynamic type) { + js_util.callMethod(glContext, 'bufferData', [kElementArrayBuffer, data, type]); } void enableVertexAttribArray(int index) { - js_util.callMethod(glContext!, 'enableVertexAttribArray', [index]); + js_util.callMethod(glContext, 'enableVertexAttribArray', [index]); } /// Clear background. void clear() { - js_util.callMethod(glContext!, 'clear', [kColorBufferBit]); + js_util.callMethod(glContext, 'clear', [kColorBufferBit]); } /// Destroys gl context. @@ -473,25 +580,29 @@ class _GlContext { } void deleteProgram(Object program) { - js_util.callMethod(glContext!, 'deleteProgram', [program]); + js_util.callMethod(glContext, 'deleteProgram', [program]); } void deleteShader(Object shader) { - js_util.callMethod(glContext!, 'deleteShader', [shader]); + js_util.callMethod(glContext, 'deleteShader', [shader]); } dynamic _getExtension(String extensionName) => - js_util.callMethod(glContext!, 'getExtension', [extensionName]); + js_util.callMethod(glContext, 'getExtension', [extensionName]); void drawTriangles(int triangleCount, ui.VertexMode vertexMode) { dynamic mode = _triangleTypeFromMode(vertexMode); - js_util.callMethod(glContext!, 'drawArrays', [mode, 0, triangleCount]); + js_util.callMethod(glContext, 'drawArrays', [mode, 0, triangleCount]); + } + + void drawElements(dynamic type, int indexCount, dynamic indexType) { + js_util.callMethod(glContext, 'drawElements', [type, indexCount, indexType, 0]); } /// Sets affine transformation from normalized device coordinates /// to window coordinates void viewport(double x, double y, double width, double height) { - js_util.callMethod(glContext!, 'viewport', [x, y, width, height]); + js_util.callMethod(glContext, 'viewport', [x, y, width, height]); } dynamic _triangleTypeFromMode(ui.VertexMode mode) { @@ -506,175 +617,225 @@ class _GlContext { } Object? _createShader(String shaderType) => js_util.callMethod( - glContext!, 'createShader', [js_util.getProperty(glContext!, shaderType)]); + glContext, 'createShader', [js_util.getProperty(glContext, shaderType)]); /// Error state of gl context. - dynamic get error => js_util.callMethod(glContext!, 'getError', const []); + dynamic get error => js_util.callMethod(glContext, 'getError', const []); /// Shader compiler error, if this returns [kFalse], to get details use /// [getShaderInfoLog]. dynamic get compileStatus => - _kCompileStatus ??= js_util.getProperty(glContext!, 'COMPILE_STATUS'); + _kCompileStatus ??= js_util.getProperty(glContext, 'COMPILE_STATUS'); dynamic get kArrayBuffer => - _kArrayBuffer ??= js_util.getProperty(glContext!, 'ARRAY_BUFFER'); + _kArrayBuffer ??= js_util.getProperty(glContext, 'ARRAY_BUFFER'); + + dynamic get kElementArrayBuffer => + _kElementArrayBuffer ??= js_util.getProperty(glContext, + 'ELEMENT_ARRAY_BUFFER'); dynamic get kLinkStatus => - _kLinkStatus ??= js_util.getProperty(glContext!, 'LINK_STATUS'); + _kLinkStatus ??= js_util.getProperty(glContext, 'LINK_STATUS'); - dynamic get kFloat => _kFloat ??= js_util.getProperty(glContext!, 'FLOAT'); + dynamic get kFloat => _kFloat ??= js_util.getProperty(glContext, 'FLOAT'); - dynamic get kRGBA => _kRGBA ??= js_util.getProperty(glContext!, 'RGBA'); + dynamic get kRGBA => _kRGBA ??= js_util.getProperty(glContext, 'RGBA'); dynamic get kUnsignedByte => - _kUnsignedByte ??= js_util.getProperty(glContext!, 'UNSIGNED_BYTE'); + _kUnsignedByte ??= js_util.getProperty(glContext, 'UNSIGNED_BYTE'); + + dynamic get kUnsignedShort => + _kUnsignedShort ??= js_util.getProperty(glContext, 'UNSIGNED_SHORT'); dynamic get kStaticDraw => - _kStaticDraw ??= js_util.getProperty(glContext!, 'STATIC_DRAW'); + _kStaticDraw ??= js_util.getProperty(glContext, 'STATIC_DRAW'); dynamic get kTriangles => - _kTriangles ??= js_util.getProperty(glContext!, 'TRIANGLES'); + _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLES'); dynamic get kTriangleFan => - _kTriangles ??= js_util.getProperty(glContext!, 'TRIANGLE_FAN'); + _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLE_FAN'); dynamic get kTriangleStrip => - _kTriangles ??= js_util.getProperty(glContext!, 'TRIANGLE_STRIP'); + _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLE_STRIP'); dynamic get kColorBufferBit => - _kColorBufferBit ??= js_util.getProperty(glContext!, 'COLOR_BUFFER_BIT'); + _kColorBufferBit ??= js_util.getProperty(glContext, 'COLOR_BUFFER_BIT'); /// Returns reference to uniform in program. - Object? getUniformLocation(Object? program, String uniformName) { + Object getUniformLocation(Object program, String uniformName) { + Object? res = js_util + .callMethod(glContext, 'getUniformLocation', [program, uniformName]); + if (res == null) { + throw Exception('$uniformName not found'); + } else { + return res; + } + } + + /// Returns reference to uniform in program. + Object getAttributeLocation(Object program, String attribName) { + Object? res = js_util + .callMethod(glContext, 'getAttribLocation', [program, attribName]); + if (res == null) { + throw Exception('$attribName not found'); + } else { + return res; + } + } + + /// Sets float uniform value. + void setUniform1f(Object uniform, double value) { return js_util - .callMethod(glContext!, 'getUniformLocation', [program, uniformName]); + .callMethod(glContext, 'uniform1f', [uniform, value]); } /// Sets vec2 uniform values. void setUniform2f(Object uniform, double value1, double value2) { return js_util - .callMethod(glContext!, 'uniform2f', [uniform, value1, value2]); + .callMethod(glContext, 'uniform2f', [uniform, value1, value2]); } /// Sets vec4 uniform values. - void setUniform4f(Object? uniform, double value1, double value2, double value3, + void setUniform4f(Object uniform, double value1, double value2, double value3, double value4) { return js_util.callMethod( - glContext!, 'uniform4f', [uniform, value1, value2, value3, value4]); + glContext, 'uniform4f', [uniform, value1, value2, value3, value4]); } /// Sets mat4 uniform values. - void setUniformMatrix4fv(Object? uniform, bool transpose, Float32List? value) { + void setUniformMatrix4fv(Object uniform, bool transpose, Float32List value) { return js_util.callMethod( - glContext!, 'uniformMatrix4fv', [uniform, transpose, value]); + glContext, 'uniformMatrix4fv', [uniform, transpose, value]); } /// Shader compile error log. dynamic getShaderInfoLog(Object glShader) { - return js_util.callMethod(glContext!, 'getShaderInfoLog', [glShader]); + return js_util.callMethod(glContext, 'getShaderInfoLog', [glShader]); } /// Errors that occurred during failed linking or validation of program /// objects. Typically called after [linkProgram]. - String? getProgramInfoLog(Object? glProgram) { - return js_util.callMethod(glContext!, 'getProgramInfoLog', [glProgram]); + String? getProgramInfoLog(Object glProgram) { + return js_util.callMethod(glContext, 'getProgramInfoLog', [glProgram]); } int? get drawingBufferWidth => - js_util.getProperty(glContext!, 'drawingBufferWidth'); + js_util.getProperty(glContext, 'drawingBufferWidth'); int? get drawingBufferHeight => - js_util.getProperty(glContext!, 'drawingBufferWidth'); + js_util.getProperty(glContext, 'drawingBufferWidth'); + /// Reads gl contents as image data. + /// + /// Warning: data is read bottom up (flipped). html.ImageData readImageData() { + const int kBytesPerPixel = 4; + final int bufferWidth = _widthInPixels!; + final int bufferHeight = _heightInPixels!; if (browserEngine == BrowserEngine.webkit || browserEngine == BrowserEngine.firefox) { - const int kBytesPerPixel = 4; - final int bufferWidth = _widthInPixels!; - final int bufferHeight = _heightInPixels!; final Uint8List pixels = Uint8List(bufferWidth * bufferHeight * kBytesPerPixel); - js_util.callMethod(glContext!, 'readPixels', + js_util.callMethod(glContext, 'readPixels', [0, 0, bufferWidth, bufferHeight, kRGBA, kUnsignedByte, pixels]); return html.ImageData( Uint8ClampedList.fromList(pixels), bufferWidth, bufferHeight); } else { - const int kBytesPerPixel = 4; - final int bufferWidth = _widthInPixels!; - final int bufferHeight = _heightInPixels!; final Uint8ClampedList pixels = Uint8ClampedList(bufferWidth * bufferHeight * kBytesPerPixel); - js_util.callMethod(glContext!, 'readPixels', + js_util.callMethod(glContext, 'readPixels', [0, 0, bufferWidth, bufferHeight, kRGBA, kUnsignedByte, pixels]); return html.ImageData(pixels, bufferWidth, bufferHeight); } } + + /// Returns image data in a form that can be used to create Canvas + /// context patterns. + Object? readPatternData() { + // When using OffscreenCanvas and transferToImageBitmap is supported by + // browser create ImageBitmap otherwise use more expensive canvas + // allocation. + if (_canvas != null && + js_util.hasProperty(_canvas!, 'transferToImageBitmap')) { + js_util.callMethod(_canvas!, 'getContext', ['webgl2']); + Object?imageBitmap = js_util.callMethod(_canvas!, 'transferToImageBitmap', + []); + return imageBitmap; + } else { + html.CanvasElement canvas = html.CanvasElement(width: _widthInPixels, height: _heightInPixels); + final html.CanvasRenderingContext2D ctx = canvas.context2D; + drawImage(ctx, 0, 0); + return canvas; + } + } } -/// Shared Cached OffscreenCanvas for webgl rendering to image. -class _OffscreenCanvas { - static html.OffscreenCanvas? _canvas; - static int _maxPixelWidth = 0; - static int _maxPixelHeight = 0; - static html.CanvasElement? _glCanvas; - static _GlContext? _cachedContext; +/// Polyfill for html.OffscreenCanvas that is not supported on some browsers. +class _OffScreenCanvas { + html.OffscreenCanvas? _canvas; + html.CanvasElement? _glCanvas; + int width; + int height; - _OffscreenCanvas(int width, int height) { - assert(width > 0 && height > 0); - if (width > _maxPixelWidth || height > _maxPixelHeight) { - // Allocate bigger offscreen canvas. + _OffScreenCanvas(this.width, this.height) { + if (_OffScreenCanvas.supported) { _canvas = html.OffscreenCanvas(width, height); - _maxPixelWidth = width; - _maxPixelHeight = height; - _cachedContext?.dispose(); - _cachedContext = null; + } else { + _glCanvas = html.CanvasElement( + width: width, + height: height, + ); + _glCanvas!.className = 'gl-canvas'; + final double cssWidth = width / EngineWindow.browserDevicePixelRatio; + final double cssHeight = height / EngineWindow.browserDevicePixelRatio; + _glCanvas!.style + ..position = 'absolute' + ..width = '${cssWidth}px' + ..height = '${cssHeight}px'; } } - static void dispose() { + void dispose() { _canvas = null; + _glCanvas = null; + } + + /// Feature detects OffscreenCanvas. + static bool get supported => + js_util.hasProperty(html.window, 'OffscreenCanvas'); +} + +/// Creates gl context from cached OffscreenCanvas for webgl rendering to image. +class _GlContextCache { + static int _maxPixelWidth = 0; + static int _maxPixelHeight = 0; + static _GlContext? _cachedContext; + static _OffScreenCanvas? _offScreenCanvas; + + static void dispose() { _maxPixelWidth = 0; _maxPixelHeight = 0; - _glCanvas = null; _cachedContext = null; + _offScreenCanvas?.dispose(); } - html.OffscreenCanvas? get canvas => _canvas; - static _GlContext? createGlContext(int widthInPixels, int heightInPixels) { - final bool isWebKit = (browserEngine == BrowserEngine.webkit); - - if (_OffscreenCanvas.supported) { - final _OffscreenCanvas offScreenCanvas = - _OffscreenCanvas(widthInPixels, heightInPixels); - _cachedContext ??= _GlContext.fromOffscreenCanvas(offScreenCanvas.canvas!); - _cachedContext!.setViewportSize(widthInPixels, heightInPixels); - return _cachedContext; + if (widthInPixels > _maxPixelWidth || heightInPixels > _maxPixelHeight) { + _cachedContext?.dispose(); + _cachedContext = null; + _offScreenCanvas = null; + _maxPixelWidth = math.max(_maxPixelWidth, widthInPixels); + _maxPixelHeight = math.max(_maxPixelHeight, widthInPixels); + } + _offScreenCanvas ??= _OffScreenCanvas(widthInPixels, heightInPixels); + if (_OffScreenCanvas.supported) { + _cachedContext ??= + _GlContext.fromOffscreenCanvas(_offScreenCanvas!._canvas!); } else { - // Allocate new canvas element is size is larger. - if (widthInPixels > _maxPixelWidth || heightInPixels > _maxPixelHeight) { - _glCanvas = html.CanvasElement( - width: widthInPixels, - height: heightInPixels, - ); - _glCanvas!.className = 'gl-canvas'; - final double cssWidth = widthInPixels / EngineWindow.browserDevicePixelRatio; - final double cssHeight = heightInPixels / EngineWindow.browserDevicePixelRatio; - _glCanvas!.style - ..position = 'absolute' - ..width = '${cssWidth}px' - ..height = '${cssHeight}px'; - _maxPixelWidth = widthInPixels; - _maxPixelHeight = heightInPixels; - _cachedContext?.dispose(); - _cachedContext = null; - } - _cachedContext ??= _GlContext.fromCanvas(_glCanvas!, isWebKit); - _cachedContext!.setViewportSize(widthInPixels, heightInPixels); - return _cachedContext; + _cachedContext ??= _GlContext.fromCanvas(_offScreenCanvas!._glCanvas!, + webGLVersion == 1); } + _cachedContext!.setViewportSize(widthInPixels, heightInPixels); + return _cachedContext; } - - /// Feature detects OffscreenCanvas. - static bool get supported => - js_util.hasProperty(html.window, 'OffscreenCanvas'); } diff --git a/lib/web_ui/lib/src/engine/html/scene_builder.dart b/lib/web_ui/lib/src/engine/html/scene_builder.dart index b9dd6f790aad9..705b3aa4bc589 100644 --- a/lib/web_ui/lib/src/engine/html/scene_builder.dart +++ b/lib/web_ui/lib/src/engine/html/scene_builder.dart @@ -113,7 +113,8 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { }) { assert(clipBehavior != null); // ignore: unnecessary_null_comparison assert(clipBehavior != ui.Clip.none); - return _pushSurface(PersistedClipRect(oldLayer as PersistedClipRect?, rect)) as ui.ClipRectEngineLayer; + return _pushSurface(PersistedClipRect(oldLayer as PersistedClipRect?, rect, clipBehavior)) + as ui.ClipRectEngineLayer; } /// Pushes a rounded-rectangular clip operation onto the operation stack. diff --git a/lib/web_ui/lib/src/engine/html/shaders/normalized_gradient.dart b/lib/web_ui/lib/src/engine/html/shaders/normalized_gradient.dart new file mode 100644 index 0000000000000..f53dc4d7fc946 --- /dev/null +++ b/lib/web_ui/lib/src/engine/html/shaders/normalized_gradient.dart @@ -0,0 +1,162 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.10 +part of engine; + +/// Converts colors and stops to typed array of bias, scale and threshold to use +/// in shaders. +/// +/// A color is generated by taking a t value [0..1] and computing +/// t * scale + bias. +/// +/// Example: For stops 0.0 t1, t2, 1.0 and colors c0, c1, c2, c3 +/// Given t1 colors, {List? stops}) { + // If colorStops is not provided, then only two stops, at 0.0 and 1.0, + // are implied (and colors must therefore only have two entries). + assert(stops != null || colors.length == 2); + stops ??= const [0.0, 1.0]; + final int colorCount = colors.length; + int normalizedCount = colorCount; + bool addFirst = stops[0] != 0.0; + bool addLast = stops.last != 1.0; + if (addFirst) { + normalizedCount++; + } + if (addLast) { + normalizedCount++; + } + final Float32List bias = Float32List(normalizedCount * 4); + final Float32List scale = Float32List(normalizedCount * 4); + final Float32List thresholds = Float32List(4 * ((normalizedCount - 1)~/4 + 1)); + int targetIndex = 0; + int thresholdIndex = 0; + if (addFirst) { + ui.Color c = colors[0]; + bias[targetIndex++] = c.red / 255.0; + bias[targetIndex++] = c.green / 255.0; + bias[targetIndex++] = c.blue / 255.0; + bias[targetIndex++] = c.alpha / 255.0; + thresholds[thresholdIndex++] = 0.0; + } + for (ui.Color c in colors) { + bias[targetIndex++] = c.red / 255.0; + bias[targetIndex++] = c.green / 255.0; + bias[targetIndex++] = c.blue / 255.0; + bias[targetIndex++] = c.alpha / 255.0; + } + for (double stop in stops) { + thresholds[thresholdIndex++] = stop; + } + if (addLast) { + ui.Color c = colors.last; + bias[targetIndex++] = c.red / 255.0; + bias[targetIndex++] = c.green / 255.0; + bias[targetIndex++] = c.blue / 255.0; + bias[targetIndex++] = c.alpha / 255.0; + thresholds[thresholdIndex++] = 1.0; + } + // Now that we have bias for each color stop, we can compute scale based + // on delta between colors. + int lastColorIndex = 4 * (normalizedCount - 1); + for (int i = 0; i < lastColorIndex; i++) { + int thresholdIndex = i >> 2; + scale[i] = (bias[i + 4] - bias[i]) / + (thresholds[thresholdIndex + 1] - thresholds[thresholdIndex]); + } + scale[lastColorIndex] = 0.0; + scale[lastColorIndex + 1] = 0.0; + scale[lastColorIndex + 2] = 0.0; + scale[lastColorIndex + 3] = 0.0; + // Compute bias = colorAtStop - stopValue * (scale). + for (int i = 0; i < normalizedCount; i++) { + double t = thresholds[i]; + int colorIndex = i * 4; + bias[colorIndex] -= t * scale[colorIndex]; + bias[colorIndex + 1] -= t * scale[colorIndex + 1]; + bias[colorIndex + 2] -= t * scale[colorIndex + 2]; + bias[colorIndex + 3] -= t * scale[colorIndex + 3]; + } + return NormalizedGradient._(normalizedCount, thresholds, scale, bias); + } + + /// Sets uniforms for threshold, bias and scale for program. + void setupUniforms(_GlContext gl, _GlProgram glProgram) { + for (int i = 0; i < thresholdCount; i++) { + Object biasId = gl.getUniformLocation(glProgram.program, 'bias_$i'); + gl.setUniform4f(biasId, _bias[i * 4], _bias[i * 4 + 1], _bias[i * 4 + 2], _bias[i * 4 + 3]); + Object scaleId = gl.getUniformLocation(glProgram.program, 'scale_$i'); + gl.setUniform4f(scaleId, _scale[i * 4], _scale[i * 4 + 1], _scale[i * 4 + 2], _scale[i * 4 + 3]); + } + for (int i = 0; i < _thresholds.length; i += 4) { + Object thresId = gl.getUniformLocation(glProgram.program, 'threshold_${i ~/ 4}'); + gl.setUniform4f(thresId, _thresholds[i], _thresholds[i + 1], _thresholds[i + 2], _thresholds[i + 3]); + } + } + + /// Returns bias component at index. + double biasAt(int index) => _bias[index]; + + /// Returns scale component at index. + double scaleAt(int index) => _scale[index]; + + /// Returns threshold at index. + double thresholdAt(int index) => _thresholds[index]; +} + +/// Writes fragment shader code to search for probe value in source data and set +/// bias and scale to be used for computation. +/// +/// Source data for thresholds is provided using ceil(count/4) packed vec4 +/// uniforms. +/// +/// Bias and scale data are vec4 uniforms that hold color data. +void _writeUnrolledBinarySearch(ShaderMethod method, int start, int end, + {required String probe, + required String sourcePrefix, required String biasName, + required String scaleName}) { + if (start == end) { + String biasSource = '${biasName}_${start}'; + method.addStatement('${biasName} = ${biasSource};'); + String scaleSource = '${scaleName}_${start}'; + method.addStatement('${scaleName} = ${scaleSource};'); + } else { + // Add probe check. + int mid = (start + end) ~/ 2; + String thresholdAtMid = '${sourcePrefix}_${(mid + 1)~/4}'; + thresholdAtMid += '.${_vectorComponentIndexToName((mid + 1) % 4)}'; + method.addStatement('if ($probe < $thresholdAtMid) {'); + method.indent(); + _writeUnrolledBinarySearch(method, start, mid, + probe: probe, sourcePrefix: sourcePrefix, biasName: biasName, + scaleName: scaleName); + method.unindent(); + method.addStatement('} else {'); + method.indent(); + _writeUnrolledBinarySearch(method, mid + 1, end, + probe: probe, sourcePrefix: sourcePrefix, biasName: biasName, + scaleName: scaleName); + method.unindent(); + method.addStatement('}'); + } +} + +String _vectorComponentIndexToName(int index) { + assert(index >=0 && index <= 4); + return 'xyzw'[index]; +} diff --git a/lib/web_ui/lib/src/engine/html/shader.dart b/lib/web_ui/lib/src/engine/html/shaders/shader.dart similarity index 53% rename from lib/web_ui/lib/src/engine/html/shader.dart rename to lib/web_ui/lib/src/engine/html/shaders/shader.dart index 4dae88f674bf9..3c79c7c983cfd 100644 --- a/lib/web_ui/lib/src/engine/html/shader.dart +++ b/lib/web_ui/lib/src/engine/html/shaders/shader.dart @@ -10,7 +10,8 @@ abstract class EngineGradient implements ui.Gradient { EngineGradient._(); /// Creates a fill style to be used in painting. - Object createPaintStyle(html.CanvasRenderingContext2D? ctx); + Object createPaintStyle(html.CanvasRenderingContext2D? ctx, + ui.Rect? shaderBounds); } class GradientSweep extends EngineGradient { @@ -22,14 +23,111 @@ class GradientSweep extends EngineGradient { assert(startAngle != null), // ignore: unnecessary_null_comparison assert(endAngle != null), // ignore: unnecessary_null_comparison assert(startAngle < endAngle), - assert(matrix4 == null || _matrix4IsValid(matrix4)), super._() { _validateColorStops(colors, colorStops); } @override - Object createPaintStyle(html.CanvasRenderingContext2D? ctx) { - throw UnimplementedError(); + Object createPaintStyle(html.CanvasRenderingContext2D? ctx, + ui.Rect? shaderBounds) { + assert(shaderBounds != null); + int widthInPixels = shaderBounds!.right.ceil(); + int heightInPixels = shaderBounds.bottom.ceil(); + assert(widthInPixels > 0 && heightInPixels > 0); + + initWebGl(); + // Render gradient into a bitmap and create a canvas pattern. + _OffScreenCanvas offScreenCanvas = + _OffScreenCanvas(widthInPixels, heightInPixels); + _GlContext gl = _OffScreenCanvas.supported + ? _GlContext.fromOffscreenCanvas(offScreenCanvas._canvas!) + : _GlContext.fromCanvas(offScreenCanvas._glCanvas!, + webGLVersion == 1); + gl.setViewportSize(widthInPixels, heightInPixels); + + NormalizedGradient normalizedGradient = NormalizedGradient( + colors, stops: colorStops); + + _GlProgram glProgram = gl.useAndCacheProgram( + _WebGlRenderer.writeBaseVertexShader(), + _createSweepFragmentShader(normalizedGradient, tileMode))!; + + Object tileOffset = gl.getUniformLocation(glProgram.program, 'u_tile_offset'); + double centerX = (center.dx - shaderBounds.left) / (shaderBounds.width); + double centerY = (center.dy - shaderBounds.top) / (shaderBounds.height); + gl.setUniform2f(tileOffset, + shaderBounds.left + 2 * (shaderBounds.width * (centerX - 0.5)), + -shaderBounds.top - 2 * (shaderBounds.height * (centerY - 0.5))); + Object angleRange = gl.getUniformLocation(glProgram.program, 'angle_range'); + gl.setUniform2f(angleRange, startAngle, endAngle); + normalizedGradient.setupUniforms(gl, glProgram); + if (matrix4 != null) { + Object gradientMatrix = gl.getUniformLocation( + glProgram.program, 'm_gradient'); + gl.setUniformMatrix4fv(gradientMatrix, false, matrix4!); + } + + Object? imageBitmap = _glRenderer!.drawRect(shaderBounds, gl, + glProgram, normalizedGradient, widthInPixels, heightInPixels); + + return ctx!.createPattern(imageBitmap!, 'no-repeat')!; + } + + String _createSweepFragmentShader(NormalizedGradient gradient, + ui.TileMode tileMode) { + ShaderBuilder builder = ShaderBuilder.fragment(webGLVersion); + builder.floatPrecision = ShaderPrecision.kMedium; + builder.addIn(ShaderType.kVec4, name: 'v_color'); + builder.addUniform(ShaderType.kVec2, name: 'u_resolution'); + builder.addUniform(ShaderType.kVec2, name: 'u_tile_offset'); + builder.addUniform(ShaderType.kVec2, name: 'angle_range'); + builder.addUniform(ShaderType.kMat4, name: 'm_gradient'); + ShaderDeclaration fragColor = builder.fragmentColor; + ShaderMethod method = builder.addMethod('main'); + // Sweep gradient + method.addStatement( + 'vec2 center = 0.5 * (u_resolution + u_tile_offset);'); + method.addStatement( + 'vec4 localCoord = vec4(gl_FragCoord.x - center.x, center.y - gl_FragCoord.y, 0, 1) * m_gradient;'); + method.addStatement( + 'float angle = atan(-localCoord.y, -localCoord.x) + ${math.pi};'); + method.addStatement( + 'float sweep = angle_range.y - angle_range.x;'); + method.addStatement( + 'angle = (angle - angle_range.x) / sweep;'); + method.addStatement('' + 'float st = angle;'); + + method.addStatement('vec4 bias;'); + method.addStatement('vec4 scale;'); + // Write uniforms for each threshold, bias and scale. + for (int i = 0; i < (gradient.thresholdCount - 1) ~/ 4 + 1; i++) { + builder.addUniform(ShaderType.kVec4, name: 'threshold_${i}'); + } + for (int i = 0; i < gradient.thresholdCount; i++) { + builder.addUniform(ShaderType.kVec4, name: 'bias_$i'); + builder.addUniform(ShaderType.kVec4, name: 'scale_$i'); + } + String probeName = 'st'; + switch (tileMode) { + case ui.TileMode.clamp: + break; + case ui.TileMode.repeated: + method.addStatement('float tiled_st = fract(st);'); + probeName = 'tiled_st'; + break; + case ui.TileMode.mirror: + method.addStatement('float t_1 = (st - 1.0);'); + method.addStatement('float tiled_st = abs((t_1 - 2.0 * floor(t_1 * 0.5)) - 1.0);'); + probeName = 'tiled_st'; + break; + } + _writeUnrolledBinarySearch(method, 0, gradient.thresholdCount - 1, + probe: probeName, sourcePrefix: 'threshold', + biasName: 'bias', scaleName: 'scale'); + method.addStatement('${fragColor.name} = ${probeName} * scale + bias;'); + String shader = builder.build(); + return shader; } final ui.Offset center; @@ -68,7 +166,8 @@ class GradientLinear extends EngineGradient { final _FastMatrix64? matrix4; @override - html.CanvasGradient createPaintStyle(html.CanvasRenderingContext2D? ctx) { + html.CanvasGradient createPaintStyle(html.CanvasRenderingContext2D? ctx, + ui.Rect? shaderBounds) { _FastMatrix64? matrix4 = this.matrix4; html.CanvasGradient gradient; if (matrix4 != null) { @@ -115,7 +214,8 @@ class GradientRadial extends EngineGradient { final Float32List? matrix4; @override - Object createPaintStyle(html.CanvasRenderingContext2D? ctx) { + Object createPaintStyle(html.CanvasRenderingContext2D? ctx, + ui.Rect? shaderBounds) { if (!experimentalUseSkia) { if (tileMode != ui.TileMode.clamp) { throw UnimplementedError( @@ -154,7 +254,8 @@ class GradientConical extends EngineGradient { final Float32List? matrix4; @override - Object createPaintStyle(html.CanvasRenderingContext2D? ctx) { + Object createPaintStyle(html.CanvasRenderingContext2D? ctx, + ui.Rect? shaderBounds) { throw UnimplementedError(); } } diff --git a/lib/web_ui/lib/src/engine/html/shaders/shader_builder.dart b/lib/web_ui/lib/src/engine/html/shaders/shader_builder.dart new file mode 100644 index 0000000000000..d5d90cb93eee0 --- /dev/null +++ b/lib/web_ui/lib/src/engine/html/shaders/shader_builder.dart @@ -0,0 +1,401 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.10 +part of engine; + +/// Creates shader program for target webgl version. +/// +/// See spec at https://www.khronos.org/registry/webgl/specs/latest/1.0/. +/// +/// Differences in WebGL2 vs WebGL1. +/// - WebGL2 needs '#version 300 es' to enable the new shading language +/// - vertex attributes have the qualifier 'in' instead of 'attribute' +/// - GLSL 3.00 defines texture and other new and future reserved words. +/// - varying is now called `in`. +/// - GLSL 1.00 has a predefined variable gl_FragColor which now needs to be +/// defined as `out vec4 fragmentColor`. +/// - Texture lookup functions texture2D and textureCube have now been +/// replaced with texture. +/// +/// Example usage: +/// ShaderBuilder builder = ShaderBuilder(WebGlVersion.webgl2); +/// ShaderDeclaration u1 = builder.addUniform(ShaderType.kVec4); +/// ShaderMethod method = builder.addMethod('main'); +/// method.addStatement('${u1.name} = vec4(1.0, 1.0, 1.0, 0.0);'); +/// source = builder.build(); +class ShaderBuilder { + /// WebGL version. + final int version; + final List declarations = []; + final List _methods = []; + + /// Precision for integer variables. + int? integerPrecision; + + /// Precision floating point variables. + int? floatPrecision; + + /// Counter for generating unique name if name is not specified for attribute. + int _attribCounter = 0; + + /// Counter for generating unique name if name is not specified for varying. + int _varyingCounter = 0; + + /// Counter for generating unique name if name is not specified for uniform. + int _uniformCounter = 0; + + /// Counter for generating unique name if name is not specified for constant. + int _constCounter = 0; + + final bool isWebGl2; + final bool _isFragmentShader; + + static const String kOpenGlEs3Header = '#version 300 es'; + + /// Lazily allocated fragment color output. + ShaderDeclaration? _fragmentColorDeclaration; + + ShaderBuilder(this.version) : isWebGl2 = version == WebGLVersion.webgl2, + _isFragmentShader = false; + + ShaderBuilder.fragment(this.version) : + isWebGl2 = version == WebGLVersion.webgl2, + _isFragmentShader = true; + + /// Returns fragment color declaration for fragment shader. + /// + /// This is hard coded for webgl1 as gl_FragColor. + ShaderDeclaration get fragmentColor { + _fragmentColorDeclaration ??= ShaderDeclaration( + isWebGl2 ? 'gFragColor' : 'gl_FragColor', + ShaderType.kVec4, + ShaderStorageQualifier.kVarying); + return _fragmentColorDeclaration!; + } + + /// Adds an attribute. + /// + /// The attribute variable is assigned a value from a object buffer as a + /// series of graphics primitives are rendered. The value is only accessible + /// in the vertex shader. + ShaderDeclaration addIn(int dataType, {String? name}) { + ShaderDeclaration attrib = ShaderDeclaration( + name ?? 'attr_${_attribCounter++}', + dataType, + ShaderStorageQualifier.kAttribute); + declarations.add(attrib); + return attrib; + } + + /// Adds a constant. + ShaderDeclaration addConst(int dataType, String value, {String? name}) { + ShaderDeclaration declaration = ShaderDeclaration.constant( + name ?? 'c_${_constCounter++}', dataType, value); + declarations.add(declaration); + return declaration; + } + + /// Adds a uniform variable. + /// + /// The variable is assigned a value before a gl.draw call. + /// It is accessible in both the vertex and fragment shaders. + /// + ShaderDeclaration addUniform(int dataType, {String? name}) { + ShaderDeclaration uniform = ShaderDeclaration( + name ?? 'uni_${_uniformCounter++}', + dataType, + ShaderStorageQualifier.kUniform); + declarations.add(uniform); + return uniform; + } + + /// Adds a varying variable. + /// + /// The variable is assigned a value by a vertex shader and + /// interpolated across the surface of a graphics primitive for each + /// input to a fragment shader. + /// It can be used in a fragment shader, but not changed. + ShaderDeclaration addOut(int dataType, {String? name}) { + ShaderDeclaration varying = ShaderDeclaration( + name ?? 'output_${_varyingCounter++}', + dataType, + ShaderStorageQualifier.kVarying); + declarations.add(varying); + return varying; + } + + void _writeVariableDeclaration(StringBuffer sb, ShaderDeclaration variable) { + switch (variable.storage) { + case ShaderStorageQualifier.kConst: + _buffer.write('const '); + break; + case ShaderStorageQualifier.kAttribute: + _buffer.write(isWebGl2 ? 'in ' + : _isFragmentShader ? 'varying ' : 'attribute '); + break; + case ShaderStorageQualifier.kUniform: + _buffer.write('uniform '); + break; + case ShaderStorageQualifier.kVarying: + _buffer.write(isWebGl2 ? 'out ' : 'varying '); + break; + } + _buffer.write('${typeToString(variable.dataType)} ${variable.name}'); + if (variable.storage == ShaderStorageQualifier.kConst) { + _buffer.write(' = ${variable.constValue}'); + } + _buffer.writeln(';'); + } + + final StringBuffer _buffer = StringBuffer(); + + static String typeToString(int dataType) { + switch (dataType) { + case ShaderType.kBool: + return 'bool'; + case ShaderType.kInt: + return 'int'; + case ShaderType.kFloat: + return 'float'; + case ShaderType.kBVec2: + return 'bvec2'; + case ShaderType.kBVec3: + return 'bvec3'; + case ShaderType.kBVec4: + return 'bvec4'; + case ShaderType.kIVec2: + return 'ivec2'; + case ShaderType.kIVec3: + return 'ivec3'; + case ShaderType.kIVec4: + return 'ivec4'; + case ShaderType.kVec2: + return 'vec2'; + case ShaderType.kVec3: + return 'vec3'; + case ShaderType.kVec4: + return 'vec4'; + case ShaderType.kMat2: + return 'mat2'; + case ShaderType.kMat3: + return 'mat3'; + case ShaderType.kMat4: + return 'mat4'; + case ShaderType.kSampler1D: + return 'sampler1D'; + case ShaderType.kSampler2D: + return 'sampler2D'; + case ShaderType.kSampler3D: + return 'sampler3D'; + case ShaderType.kVoid: + return 'void'; + } + throw ArgumentError(); + } + + ShaderMethod addMethod(String name) { + final ShaderMethod method = ShaderMethod(name); + _methods.add(method); + return method; + } + + String build() { + // Write header. + if (isWebGl2) { + _buffer.writeln(kOpenGlEs3Header); + } + // Write optional precision. + if (integerPrecision != null) { + _buffer + .writeln('precision ${_precisionToString(integerPrecision!)} int;'); + } + if (floatPrecision != null) { + _buffer + .writeln('precision ${_precisionToString(floatPrecision!)} float;'); + } + if (isWebGl2 && _fragmentColorDeclaration != null) { + _writeVariableDeclaration(_buffer, _fragmentColorDeclaration!); + } + for (ShaderDeclaration decl in declarations) { + _writeVariableDeclaration(_buffer, decl); + } + for (ShaderMethod method in _methods) { + method.write(_buffer); + } + return _buffer.toString(); + } + + String _precisionToString(int precision) => precision == ShaderPrecision.kLow + ? 'lowp' + : precision == ShaderPrecision.kMedium ? 'mediump' : 'highp'; +} + +class ShaderMethod { + ShaderMethod(this.name); + + final String returnType = 'void'; + final String name; + final List _statements = []; + int _indentLevel = 1; + + void indent() { + ++_indentLevel; + } + + void unindent() { + assert(_indentLevel != 1); + --_indentLevel; + } + + void addStatement(String statement) { + if (assertionsEnabled) { + _statements.add(' ' * _indentLevel + statement); + } else { + _statements.add(statement); + } + } + + void write(StringBuffer buffer) { + buffer.writeln('$returnType $name() {'); + for (String statement in _statements) { + buffer.writeln(statement); + } + buffer.writeln('}'); + } +} + +/// html webgl version qualifier constants. +abstract class WebGLVersion { + // WebGL 1.0 is based on OpenGL ES 2.0 / GLSL 1.00 + static const int webgl1 = 1; + // WebGL 2.0 is based on OpenGL ES 3.0 / GLSL 3.00 + static const int webgl2 = 2; +} + +/// WebGl Shader data types. +abstract class ShaderType { + // Basic types. + static const int kBool = 0; + static const int kInt = 1; + static const int kFloat = 2; + // Vector types. + static const int kBVec2 = 3; + static const int kBVec3 = 4; + static const int kBVec4 = 5; + static const int kIVec2 = 6; + static const int kIVec3 = 7; + static const int kIVec4 = 8; + static const int kVec2 = 9; + static const int kVec3 = 10; + static const int kVec4 = 11; + static const int kMat2 = 12; + static const int kMat3 = 13; + static const int kMat4 = 14; + // Textures. + static const int kSampler1D = 15; + static const int kSampler2D = 16; + static const int kSampler3D = 17; + // Other. + static const int kVoid = 18; +} + +/// Precision of int and float types. +/// +/// Integers: 8 bit, 10 bit and 16 bits. +/// Float: 8 bit. 14 bit and 62 bits. +abstract class ShaderPrecision { + static const int kLow = 0; + static const int kMedium = 1; + static const int kHigh = 2; +} + +/// GL Variable storage qualifiers. +abstract class ShaderStorageQualifier { + static const int kConst = 0; + static const int kAttribute = 1; + static const int kUniform = 2; + static const int kVarying = 3; +} + +/// Shader variable and constant declaration. +class ShaderDeclaration { + final String name; + final int dataType; + final int storage; + final String constValue; + ShaderDeclaration(this.name, this.dataType, this.storage) + : assert(!_isGLSLReservedWord(name)), + constValue = ''; + + /// Constructs a constant. + ShaderDeclaration.constant(this.name, this.dataType, this.constValue) + : storage = ShaderStorageQualifier.kConst; +} + +// These are used only in debug mode to assert if used as variable name. +// https://www.khronos.org/registry/OpenGL/specs/gl/GLSLangSpec.4.10.pdf +const List _kReservedWords = [ + 'attribute', + 'const', + 'uniform', + 'varying', + 'layout', + 'centroid', + 'flat', + 'smooth', + 'noperspective', + 'patch', 'sample', + 'break', 'continue', + 'do', 'for', 'while', 'switch', 'case', 'default', 'if', 'else', + 'subroutine', + 'in', 'out', 'inout', 'float', 'double', 'int', + 'void', + 'bool', 'true', 'false', + 'invariant', + 'discard', 'return', + 'mat2', 'mat3', 'mat4', 'dmat2', 'dmat3', 'dmat4', + 'mat2x2', 'mat2x3', 'mat2x4', 'dmat2x2', 'dmat2x3', 'dmat2x4', + 'mat3x2', 'mat3x3', 'mat3x4', 'dmat3x2', 'dmat3x3', 'dmat3x4', + 'mat4x2', 'mat4x3', 'mat4x4', 'dmat4x2', 'dmat4x3', 'dmat4x4', + 'vec2', 'vec3', 'vec4', 'ivec2', 'ivec3', 'ivec4', 'bvec2', 'bvec3', 'bvec4', + 'dvec2', 'dvec3', 'dvec4', + 'uint', 'uvec2', 'uvec3', 'uvec4', + 'lowp', 'mediump', 'highp', 'precision', + 'sampler1D', 'sampler2D', 'sampler3D', 'samplerCube', + 'sampler1DShadow', 'sampler2DShadow', 'samplerCubeShadow', + 'sampler1DArray', 'sampler2DArray', + 'sampler1DArrayShadow', 'sampler2DArrayShadow', + 'isampler1D', 'isampler2D', 'isampler3D', 'isamplerCube', + 'isampler1DArray', 'isampler2DArray', + 'usampler1D', 'usampler2D', 'usampler3D', 'usamplerCube', + 'usampler1DArray', 'usampler2DArray', + 'sampler2DRect', 'sampler2DRectShadow', 'isampler2DRect', 'usampler2DRect', + 'samplerBuffer', 'isamplerBuffer', 'usamplerBuffer', + 'sampler2DMS', 'isampler2DMS', 'usampler2DMS', + 'sampler2DMSArray', 'isampler2DMSArray', 'usampler2DMSArray', + 'samplerCubeArray', 'samplerCubeArrayShadow', 'isamplerCubeArray', + 'usamplerCubeArray', + 'struct', + 'texture', + + // Reserved for future use, see + // https://www.khronos.org/registry/OpenGL/specs/gl/GLSLangSpec.4.10.pdf + 'active', 'asm', 'cast', 'class', 'common', 'enum', 'extern', 'external', + 'filter', 'fixed', 'fvec2', 'fvec3', 'fvec4', 'goto', 'half', 'hvec2', + 'hvec3', 'hvec4', 'iimage1D', 'iimage1DArray', 'iimage2D', 'iimage2DArray', + 'iimage3D', 'iimageBuffer', 'iimageCube', 'image1D', 'image1DArray', + 'image1DArrayShadow', 'image1DShadow', 'image2D', 'image2DArray', + 'image2DArrayShadow', 'image2DShadow', 'image3D', 'imageBuffer', + 'imageCube', 'inline', 'input', 'interface', 'long', + 'namespace', 'noinline', 'output', 'packed', 'partition', 'public', + 'row_majo', 'short', 'sizeof', 'static', 'superp', 'template', 'this', + 'typedef', 'uimage1D', 'uimage1DArray', 'uimage2D', 'uimage2DArray', + 'uimage3D', 'uimageBuffer', 'uimageCube', 'union', 'unsigned', + 'using', 'volatile', +]; + +bool _isGLSLReservedWord(String name) { + return _kReservedWords.contains(name); +} diff --git a/lib/web_ui/lib/src/engine/keyboard.dart b/lib/web_ui/lib/src/engine/keyboard.dart index 558e53f6f927a..c15886bd6b1e0 100644 --- a/lib/web_ui/lib/src/engine/keyboard.dart +++ b/lib/web_ui/lib/src/engine/keyboard.dart @@ -177,7 +177,7 @@ int _getMetaState(html.KeyboardEvent event) { if (event.getModifierState('Shift')) { metaState |= _modifierShift; } - if (event.getModifierState('Alt')) { + if (event.getModifierState('Alt') || event.getModifierState('AltGraph')) { metaState |= _modifierAlt; } if (event.getModifierState('Control')) { diff --git a/lib/web_ui/lib/src/engine/history.dart b/lib/web_ui/lib/src/engine/navigation/history.dart similarity index 69% rename from lib/web_ui/lib/src/engine/history.dart rename to lib/web_ui/lib/src/engine/navigation/history.dart index 59e1ba5fddf6d..0a578162a9096 100644 --- a/lib/web_ui/lib/src/engine/history.dart +++ b/lib/web_ui/lib/src/engine/navigation/history.dart @@ -25,64 +25,39 @@ abstract class BrowserHistory { late ui.VoidCallback _unsubscribe; /// The strategy to interact with html browser history. - LocationStrategy? get locationStrategy => _locationStrategy; - LocationStrategy? _locationStrategy; - /// Updates the strategy. - /// - /// This method will also remove any previous modifications to the html - /// browser history and start anew. - Future setLocationStrategy(LocationStrategy? strategy) async { - if (strategy != _locationStrategy) { - await _tearoffStrategy(_locationStrategy); - _locationStrategy = strategy; - await _setupStrategy(_locationStrategy); - } - } + UrlStrategy? get urlStrategy; - Future _setupStrategy(LocationStrategy? strategy) async { - if (strategy == null) { - return; - } - _unsubscribe = strategy.onPopState(onPopState as dynamic Function(html.Event)); - await setup(); - } + bool _isDisposed = false; - Future _tearoffStrategy(LocationStrategy? strategy) async { - if (strategy == null) { - return; - } - _unsubscribe(); - - await tearDown(); + void _setupStrategy(UrlStrategy strategy) { + _unsubscribe = strategy.addPopStateListener( + onPopState as html.EventListener, + ); } /// Exit this application and return to the previous page. Future exit() async { - if (_locationStrategy != null) { - await _tearoffStrategy(_locationStrategy); + if (urlStrategy != null) { + await tearDown(); // Now the history should be in the original state, back one more time to // exit the application. - await _locationStrategy!.back(); - _locationStrategy = null; + await urlStrategy!.go(-1); } } /// This method does the same thing as the browser back button. - Future back() { - if (locationStrategy != null) { - return locationStrategy!.back(); - } - return Future.value(); + Future back() async { + return urlStrategy?.go(-1); } /// The path of the current location of the user's browser. - String get currentPath => locationStrategy?.path ?? '/'; + String get currentPath => urlStrategy?.getPath() ?? '/'; /// The state of the current location of the user's browser. - dynamic get currentState => locationStrategy?.state; + Object? get currentState => urlStrategy?.getState(); /// Update the url with the given [routeName] and [state]. - void setRouteName(String? routeName, {dynamic? state}); + void setRouteName(String? routeName, {Object? state}); /// A callback method to handle browser backward or forward buttons. /// @@ -90,12 +65,9 @@ abstract class BrowserHistory { /// applications accordingly. void onPopState(covariant html.PopStateEvent event); - /// Sets up any prerequisites to use this browser history class. - Future setup() => Future.value(); - /// Restore any modifications to the html browser history during the lifetime /// of this class. - Future tearDown() => Future.value(); + Future tearDown(); } /// A browser history class that creates a set of browser history entries to @@ -113,31 +85,51 @@ abstract class BrowserHistory { /// * [SingleEntryBrowserHistory], which is used when the framework does not use /// a Router for routing. class MultiEntriesBrowserHistory extends BrowserHistory { + MultiEntriesBrowserHistory({required this.urlStrategy}) { + final UrlStrategy? strategy = urlStrategy; + if (strategy == null) { + return; + } + + _setupStrategy(strategy); + if (!_hasSerialCount(currentState)) { + strategy.replaceState( + _tagWithSerialCount(currentState, 0), 'flutter', currentPath); + } + // If we restore from a page refresh, the _currentSerialCount may not be 0. + _lastSeenSerialCount = _currentSerialCount; + } + + @override + final UrlStrategy? urlStrategy; + late int _lastSeenSerialCount; int get _currentSerialCount { if (_hasSerialCount(currentState)) { - return currentState['serialCount'] as int; + final Map stateMap = + currentState as Map; + return stateMap['serialCount'] as int; } return 0; } - dynamic _tagWithSerialCount(dynamic originialState, int count) { - return { + Object _tagWithSerialCount(Object? originialState, int count) { + return { 'serialCount': count, 'state': originialState, }; } - bool _hasSerialCount(dynamic state) { + bool _hasSerialCount(Object? state) { return state is Map && state['serialCount'] != null; } @override - void setRouteName(String? routeName, {dynamic? state}) { - if (locationStrategy != null) { + void setRouteName(String? routeName, {Object? state}) { + if (urlStrategy != null) { assert(routeName != null); _lastSeenSerialCount += 1; - locationStrategy!.pushState( + urlStrategy!.pushState( _tagWithSerialCount(state, _lastSeenSerialCount), 'flutter', routeName!, @@ -147,58 +139,51 @@ class MultiEntriesBrowserHistory extends BrowserHistory { @override void onPopState(covariant html.PopStateEvent event) { - assert(locationStrategy != null); + assert(urlStrategy != null); // May be a result of direct url access while the flutter application is // already running. if (!_hasSerialCount(event.state)) { // In this case we assume this will be the next history entry from the // last seen entry. - locationStrategy!.replaceState( - _tagWithSerialCount(event.state, _lastSeenSerialCount + 1), - 'flutter', - currentPath); + urlStrategy!.replaceState( + _tagWithSerialCount(event.state, _lastSeenSerialCount + 1), + 'flutter', + currentPath); } _lastSeenSerialCount = _currentSerialCount; if (window._onPlatformMessage != null) { window.invokeOnPlatformMessage( 'flutter/navigation', const JSONMethodCodec().encodeMethodCall( - MethodCall('pushRouteInformation', { - 'location': currentPath, - 'state': event.state?['state'], - }) - ), + MethodCall('pushRouteInformation', { + 'location': currentPath, + 'state': event.state?['state'], + })), (_) {}, ); } } @override - Future setup() { - if (!_hasSerialCount(currentState)) { - locationStrategy!.replaceState( - _tagWithSerialCount(currentState, 0), - 'flutter', - currentPath - ); + Future tearDown() async { + if (_isDisposed || urlStrategy == null) { + return; } - // If we retore from a page refresh, the _currentSerialCount may not be 0. - _lastSeenSerialCount = _currentSerialCount; - return Future.value(); - } + _isDisposed = true; + _unsubscribe(); - @override - Future tearDown() async { // Restores the html browser history. assert(_hasSerialCount(currentState)); int backCount = _currentSerialCount; if (backCount > 0) { - await locationStrategy!.back(count: backCount); + await urlStrategy!.go(-backCount); } // Unwrap state. assert(_hasSerialCount(currentState) && _currentSerialCount == 0); - locationStrategy!.replaceState( - currentState['state'], + final Map stateMap = + currentState as Map; + urlStrategy!.replaceState( + stateMap['state'], 'flutter', currentPath, ); @@ -222,37 +207,61 @@ class MultiEntriesBrowserHistory extends BrowserHistory { /// * [MultiEntriesBrowserHistory], which is used when the framework uses a /// Router for routing. class SingleEntryBrowserHistory extends BrowserHistory { + SingleEntryBrowserHistory({required this.urlStrategy}) { + final UrlStrategy? strategy = urlStrategy; + if (strategy == null) { + return; + } + + _setupStrategy(strategy); + + final String path = currentPath; + if (!_isFlutterEntry(html.window.history.state)) { + // An entry may not have come from Flutter, for example, when the user + // refreshes the page. They land directly on the "flutter" entry, so + // there's no need to setup the "origin" and "flutter" entries, we can + // safely assume they are already setup. + _setupOriginEntry(strategy); + _setupFlutterEntry(strategy, replace: false, path: path); + } + } + + @override + final UrlStrategy? urlStrategy; + static const MethodCall _popRouteMethodCall = MethodCall('popRoute'); static const String _kFlutterTag = 'flutter'; static const String _kOriginTag = 'origin'; - Map _wrapOriginState(dynamic state) { + Map _wrapOriginState(Object? state) { return {_kOriginTag: true, 'state': state}; } - dynamic _unwrapOriginState(dynamic state) { + + Object? _unwrapOriginState(Object? state) { assert(_isOriginEntry(state)); final Map originState = state as Map; return originState['state']; } + Map _flutterState = {_kFlutterTag: true}; /// The origin entry is the history entry that the Flutter app landed on. It's /// created by the browser when the user navigates to the url of the app. - bool _isOriginEntry(dynamic state) { + bool _isOriginEntry(Object? state) { return state is Map && state[_kOriginTag] == true; } /// The flutter entry is a history entry that we maintain on top of the origin /// entry. It allows us to catch popstate events when the user hits the back /// button. - bool _isFlutterEntry(dynamic state) { + bool _isFlutterEntry(Object? state) { return state is Map && state[_kFlutterTag] == true; } @override - void setRouteName(String? routeName, {dynamic? state}) { - if (locationStrategy != null) { - _setupFlutterEntry(locationStrategy!, replace: true, path: routeName); + void setRouteName(String? routeName, {Object? state}) { + if (urlStrategy != null) { + _setupFlutterEntry(urlStrategy!, replace: true, path: routeName); } } @@ -260,7 +269,7 @@ class SingleEntryBrowserHistory extends BrowserHistory { @override void onPopState(covariant html.PopStateEvent event) { if (_isOriginEntry(event.state)) { - _setupFlutterEntry(_locationStrategy!); + _setupFlutterEntry(urlStrategy!); // 2. Send a 'popRoute' platform message so the app can handle it accordingly. if (window._onPlatformMessage != null) { @@ -302,14 +311,14 @@ class SingleEntryBrowserHistory extends BrowserHistory { // 2. Then we remove the new entry. // This will take us back to our "flutter" entry and it causes a new // popstate event that will be handled in the "else if" section above. - _locationStrategy!.back(); + urlStrategy!.go(-1); } } /// This method should be called when the Origin Entry is active. It just /// replaces the state of the entry so that we can recognize it later using /// [_isOriginEntry] inside [_popStateListener]. - void _setupOriginEntry(LocationStrategy strategy) { + void _setupOriginEntry(UrlStrategy strategy) { assert(strategy != null); // ignore: unnecessary_null_comparison strategy.replaceState(_wrapOriginState(currentState), 'origin', ''); } @@ -317,7 +326,7 @@ class SingleEntryBrowserHistory extends BrowserHistory { /// This method is used manipulate the Flutter Entry which is always the /// active entry while the Flutter app is running. void _setupFlutterEntry( - LocationStrategy strategy, { + UrlStrategy strategy, { bool replace = false, String? path, }) { @@ -330,28 +339,18 @@ class SingleEntryBrowserHistory extends BrowserHistory { } } - @override - Future setup() { - final String path = currentPath; - if (_isFlutterEntry(html.window.history.state)) { - // This could happen if the user, for example, refreshes the page. They - // will land directly on the "flutter" entry, so there's no need to setup - // the "origin" and "flutter" entries, we can safely assume they are - // already setup. - } else { - _setupOriginEntry(locationStrategy!); - _setupFlutterEntry(locationStrategy!, replace: false, path: path); - } - return Future.value(); - } - @override Future tearDown() async { - if (locationStrategy != null) { - // We need to remove the flutter entry that we pushed in setup. - await locationStrategy!.back(); - // Restores original state. - locationStrategy!.replaceState(_unwrapOriginState(currentState), 'flutter', currentPath); + if (_isDisposed || urlStrategy == null) { + return; } + _isDisposed = true; + _unsubscribe(); + + // We need to remove the flutter entry that we pushed in setup. + await urlStrategy!.go(-1); + // Restores original state. + urlStrategy! + .replaceState(_unwrapOriginState(currentState), 'flutter', currentPath); } } diff --git a/lib/web_ui/lib/src/engine/navigation/js_url_strategy.dart b/lib/web_ui/lib/src/engine/navigation/js_url_strategy.dart new file mode 100644 index 0000000000000..decb7c249d44d --- /dev/null +++ b/lib/web_ui/lib/src/engine/navigation/js_url_strategy.dart @@ -0,0 +1,78 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.10 +part of engine; + +typedef _PathGetter = String Function(); + +typedef _StateGetter = Object? Function(); + +typedef _AddPopStateListener = ui.VoidCallback Function(html.EventListener); + +typedef _StringToString = String Function(String); + +typedef _StateOperation = void Function( + Object? state, String title, String url); + +typedef _HistoryMove = Future Function(int count); + +/// The JavaScript representation of a URL strategy. +/// +/// This is used to pass URL strategy implementations across a JS-interop +/// bridge from the app to the engine. +@JS() +@anonymous +abstract class JsUrlStrategy { + /// Creates an instance of [JsUrlStrategy] from a bag of URL strategy + /// functions. + external factory JsUrlStrategy({ + required _PathGetter getPath, + required _StateGetter getState, + required _AddPopStateListener addPopStateListener, + required _StringToString prepareExternalUrl, + required _StateOperation pushState, + required _StateOperation replaceState, + required _HistoryMove go, + }); + + /// Adds a listener to the `popstate` event and returns a function that, when + /// invoked, removes the listener. + external ui.VoidCallback addPopStateListener(html.EventListener fn); + + /// Returns the active path in the browser. + external String getPath(); + + /// Returns the history state in the browser. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/API/History/state + external Object? getState(); + + /// Given a path that's internal to the app, create the external url that + /// will be used in the browser. + external String prepareExternalUrl(String internalUrl); + + /// Push a new history entry. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/API/History/pushState + external void pushState(Object? state, String title, String url); + + /// Replace the currently active history entry. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState + external void replaceState(Object? state, String title, String url); + + /// Moves forwards or backwards through the history stack. + /// + /// A negative [count] value causes a backward move in the history stack. And + /// a positive [count] value causs a forward move. + /// + /// Examples: + /// + /// * `go(-2)` moves back 2 steps in history. + /// * `go(3)` moves forward 3 steps in hisotry. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/API/History/go + external Future go(int count); +} diff --git a/lib/web_ui/lib/src/engine/navigation/url_strategy.dart b/lib/web_ui/lib/src/engine/navigation/url_strategy.dart new file mode 100644 index 0000000000000..fcf2cecfd0b8e --- /dev/null +++ b/lib/web_ui/lib/src/engine/navigation/url_strategy.dart @@ -0,0 +1,296 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.10 +part of engine; + +/// Represents and reads route state from the browser's URL. +/// +/// By default, the [HashUrlStrategy] subclass is used if the app doesn't +/// specify one. +abstract class UrlStrategy { + /// Abstract const constructor. This constructor enables subclasses to provide + /// const constructors so that they can be used in const expressions. + const UrlStrategy(); + + /// Adds a listener to the `popstate` event and returns a function that, when + /// invoked, removes the listener. + ui.VoidCallback addPopStateListener(html.EventListener fn); + + /// Returns the active path in the browser. + String getPath(); + + /// The state of the current browser history entry. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/API/History/state + Object? getState(); + + /// Given a path that's internal to the app, create the external url that + /// will be used in the browser. + String prepareExternalUrl(String internalUrl); + + /// Push a new history entry. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/API/History/pushState + void pushState(Object? state, String title, String url); + + /// Replace the currently active history entry. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState + void replaceState(Object? state, String title, String url); + + /// Moves forwards or backwards through the history stack. + /// + /// A negative [count] value causes a backward move in the history stack. And + /// a positive [count] value causs a forward move. + /// + /// Examples: + /// + /// * `go(-2)` moves back 2 steps in history. + /// * `go(3)` moves forward 3 steps in hisotry. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/API/History/go + Future go(int count); +} + +/// This is an implementation of [UrlStrategy] that uses the browser URL's +/// [hash fragments](https://en.wikipedia.org/wiki/Uniform_Resource_Locator#Syntax) +/// to represent its state. +/// +/// In order to use this [UrlStrategy] for an app, it needs to be set like this: +/// +/// ```dart +/// import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +/// +/// // Somewhere before calling `runApp()` do: +/// setUrlStrategy(const HashUrlStrategy()); +/// ``` +class HashUrlStrategy extends UrlStrategy { + /// Creates an instance of [HashUrlStrategy]. + /// + /// The [PlatformLocation] parameter is useful for testing to mock out browser + /// interations. + const HashUrlStrategy( + [this._platformLocation = const BrowserPlatformLocation()]); + + final PlatformLocation _platformLocation; + + @override + ui.VoidCallback addPopStateListener(html.EventListener fn) { + _platformLocation.addPopStateListener(fn); + return () => _platformLocation.removePopStateListener(fn); + } + + @override + String getPath() { + // the hash value is always prefixed with a `#` + // and if it is empty then it will stay empty + final String path = _platformLocation.hash ?? ''; + assert(path.isEmpty || path.startsWith('#')); + + // We don't want to return an empty string as a path. Instead we default to "/". + if (path.isEmpty || path == '#') { + return '/'; + } + // At this point, we know [path] starts with "#" and isn't empty. + return path.substring(1); + } + + @override + Object? getState() => _platformLocation.state; + + @override + String prepareExternalUrl(String internalUrl) { + // It's convention that if the hash path is empty, we omit the `#`; however, + // if the empty URL is pushed it won't replace any existing fragment. So + // when the hash path is empty, we instead return the location's path and + // query. + return internalUrl.isEmpty + ? '${_platformLocation.pathname}${_platformLocation.search}' + : '#$internalUrl'; + } + + @override + void pushState(Object? state, String title, String url) { + _platformLocation.pushState(state, title, prepareExternalUrl(url)); + } + + @override + void replaceState(Object? state, String title, String url) { + _platformLocation.replaceState(state, title, prepareExternalUrl(url)); + } + + @override + Future go(int count) { + _platformLocation.go(count); + return _waitForPopState(); + } + + /// Waits until the next popstate event is fired. + /// + /// This is useful, for example, to wait until the browser has handled the + /// `history.back` transition. + Future _waitForPopState() { + final Completer completer = Completer(); + late ui.VoidCallback unsubscribe; + unsubscribe = addPopStateListener((_) { + unsubscribe(); + completer.complete(); + }); + return completer.future; + } +} + +/// Wraps a custom implementation of [UrlStrategy] that was previously converted +/// to a [JsUrlStrategy]. +class CustomUrlStrategy extends UrlStrategy { + /// Wraps the [delegate] in a [CustomUrlStrategy] instance. + CustomUrlStrategy.fromJs(this.delegate); + + final JsUrlStrategy delegate; + + @override + ui.VoidCallback addPopStateListener(html.EventListener fn) => + delegate.addPopStateListener(fn); + + @override + String getPath() => delegate.getPath(); + + @override + Object? getState() => delegate.getState(); + + @override + String prepareExternalUrl(String internalUrl) => + delegate.prepareExternalUrl(internalUrl); + + @override + void pushState(Object? state, String title, String url) => + delegate.pushState(state, title, url); + + @override + void replaceState(Object? state, String title, String url) => + delegate.replaceState(state, title, url); + + @override + Future go(int count) => delegate.go(count); +} + +/// Encapsulates all calls to DOM apis, which allows the [UrlStrategy] classes +/// to be platform agnostic and testable. +/// +/// For convenience, the [PlatformLocation] class can be used by implementations +/// of [UrlStrategy] to interact with DOM apis like pushState, popState, etc. +abstract class PlatformLocation { + /// Abstract const constructor. This constructor enables subclasses to provide + /// const constructors so that they can be used in const expressions. + const PlatformLocation(); + + /// Registers an event listener for the `popstate` event. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onpopstate + void addPopStateListener(html.EventListener fn); + + /// Unregisters the given listener (added by [addPopStateListener]) from the + /// `popstate` event. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onpopstate + void removePopStateListener(html.EventListener fn); + + /// The `pathname` part of the URL in the browser address bar. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/API/Location/pathname + String get pathname; + + /// The `query` part of the URL in the browser address bar. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/API/Location/search + String get search; + + /// The `hash]` part of the URL in the browser address bar. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/API/Location/hash + String? get hash; + + /// The `state` in the current history entry. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/API/History/state + Object? get state; + + /// Adds a new entry to the browser history stack. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/API/History/pushState + void pushState(Object? state, String title, String url); + + /// Replaces the current entry in the browser history stack. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState + void replaceState(Object? state, String title, String url); + + /// Moves forwards or backwards through the history stack. + /// + /// A negative [count] value causes a backward move in the history stack. And + /// a positive [count] value causs a forward move. + /// + /// Examples: + /// + /// * `go(-2)` moves back 2 steps in history. + /// * `go(3)` moves forward 3 steps in hisotry. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/API/History/go + void go(int count); + + /// The base href where the Flutter app is being served. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base + String? getBaseHref(); +} + +/// Delegates to real browser APIs to provide platform location functionality. +class BrowserPlatformLocation extends PlatformLocation { + /// Default constructor for [BrowserPlatformLocation]. + const BrowserPlatformLocation(); + + html.Location get _location => html.window.location; + html.History get _history => html.window.history; + + @override + void addPopStateListener(html.EventListener fn) { + html.window.addEventListener('popstate', fn); + } + + @override + void removePopStateListener(html.EventListener fn) { + html.window.removeEventListener('popstate', fn); + } + + @override + String get pathname => _location.pathname!; + + @override + String get search => _location.search!; + + @override + String get hash => _location.hash; + + @override + Object? get state => _history.state; + + @override + void pushState(Object? state, String title, String url) { + _history.pushState(state, title, url); + } + + @override + void replaceState(Object? state, String title, String url) { + _history.replaceState(state, title, url); + } + + @override + void go(int count) { + _history.go(count); + } + + @override + String? getBaseHref() => html.document.baseUri; +} diff --git a/lib/web_ui/lib/src/engine/picture.dart b/lib/web_ui/lib/src/engine/picture.dart index 946f348a66a3e..42304da062c9d 100644 --- a/lib/web_ui/lib/src/engine/picture.dart +++ b/lib/web_ui/lib/src/engine/picture.dart @@ -47,7 +47,7 @@ class EnginePicture implements ui.Picture { @override Future toImage(int width, int height) async { final ui.Rect imageRect = ui.Rect.fromLTRB(0, 0, width.toDouble(), height.toDouble()); - final BitmapCanvas canvas = BitmapCanvas(imageRect); + final BitmapCanvas canvas = BitmapCanvas.imageData(imageRect); recordingCanvas!.apply(canvas, imageRect); final String imageDataUrl = canvas.toDataUrl(); final html.ImageElement imageElement = html.ImageElement() diff --git a/lib/web_ui/lib/src/engine/test_embedding.dart b/lib/web_ui/lib/src/engine/test_embedding.dart index f0d3a4291dbad..0255e5fb19601 100644 --- a/lib/web_ui/lib/src/engine/test_embedding.dart +++ b/lib/web_ui/lib/src/engine/test_embedding.dart @@ -20,29 +20,27 @@ class TestHistoryEntry { } } -/// This location strategy mimics the browser's history as closely as possible +/// This URL strategy mimics the browser's history as closely as possible /// while doing it all in memory with no interaction with the browser. /// /// It keeps a list of history entries and event listeners in memory and /// manipulates them in order to achieve the desired functionality. -class TestLocationStrategy extends LocationStrategy { - /// Creates a instance of [TestLocationStrategy] with an empty string as the +class TestUrlStrategy extends UrlStrategy { + /// Creates a instance of [TestUrlStrategy] with an empty string as the /// path. - factory TestLocationStrategy() => TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '')); + factory TestUrlStrategy() => TestUrlStrategy.fromEntry(TestHistoryEntry(null, null, '')); - /// Creates an instance of [TestLocationStrategy] and populates it with a list + /// Creates an instance of [TestUrlStrategy] and populates it with a list /// that has [initialEntry] as the only item. - TestLocationStrategy.fromEntry(TestHistoryEntry initialEntry) + TestUrlStrategy.fromEntry(TestHistoryEntry initialEntry) : _currentEntryIndex = 0, history = [initialEntry]; @override - String get path => currentEntry.url; + String getPath() => currentEntry.url; @override - dynamic get state { - return currentEntry.state; - } + dynamic getState() => currentEntry.state; int _currentEntryIndex; int get currentEntryIndex => _currentEntryIndex; @@ -105,12 +103,12 @@ class TestLocationStrategy extends LocationStrategy { } @override - Future back({int count = 1}) { + Future go(int count) { assert(withinAppHistory); - // Browsers don't move back in history immediately. They do it at the next + // Browsers don't move in history immediately. They do it at the next // event loop. So let's simulate that. return _nextEventLoop(() { - _currentEntryIndex = _currentEntryIndex - count; + _currentEntryIndex = _currentEntryIndex + count; if (withinAppHistory) { _firePopStateEvent(); } @@ -124,7 +122,7 @@ class TestLocationStrategy extends LocationStrategy { final List listeners = []; @override - ui.VoidCallback onPopState(html.EventListener fn) { + ui.VoidCallback addPopStateListener(html.EventListener fn) { listeners.add(fn); return () { // Schedule a micro task here to avoid removing the listener during diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 5d298bed3df66..ce8b31d5fd23a 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -11,6 +11,16 @@ bool _debugVisibleTextEditing = false; /// The `keyCode` of the "Enter" key. const int _kReturnKeyCode = 13; +/// Blink and Webkit engines, bring an overlay on top of the text field when it +/// is autofilled. +bool browserHasAutofillOverlay() => + browserEngine == BrowserEngine.blink || + browserEngine == BrowserEngine.webkit; + +/// `transparentTextEditing` class is configured to make the autofill overlay +/// transparent. +const String transparentTextEditingClass = 'transparentTextEditing'; + void _emptyCallback(dynamic _) {} /// These style attributes are constant throughout the life time of an input @@ -39,7 +49,11 @@ void _setStaticStyleAttributes(html.HtmlElement domElement) { ..overflow = 'hidden' ..transformOrigin = '0 0 0'; - /// This property makes the input's blinking cursor transparent. + if (browserHasAutofillOverlay()) { + domElement.classes.add(transparentTextEditingClass); + } + + // This property makes the input's blinking cursor transparent. elementStyle.setProperty('caret-color', 'transparent'); if (_debugVisibleTextEditing) { @@ -80,6 +94,10 @@ void _hideAutofillElements(html.HtmlElement domElement, ..left = '-9999px'; } + if (browserHasAutofillOverlay()) { + domElement.classes.add(transparentTextEditingClass); + } + /// This property makes the input's blinking cursor transparent. elementStyle.setProperty('caret-color', 'transparent'); } diff --git a/lib/web_ui/lib/src/engine/util.dart b/lib/web_ui/lib/src/engine/util.dart index 277f5484e2f07..4d0951527978f 100644 --- a/lib/web_ui/lib/src/engine/util.dart +++ b/lib/web_ui/lib/src/engine/util.dart @@ -507,6 +507,12 @@ class _FastMatrix64 { transformedX = matrix[12] + (matrix[0] * x) + (matrix[4] * y); transformedY = matrix[13] + (matrix[1] * x) + (matrix[5] * y); } + + String debugToString() => + '${matrix[0].toStringAsFixed(3)}, ${matrix[4].toStringAsFixed(3)}, ${matrix[8].toStringAsFixed(3)}, ${matrix[12].toStringAsFixed(3)}\n' + '${matrix[1].toStringAsFixed(3)}, ${matrix[5].toStringAsFixed(3)}, ${matrix[9].toStringAsFixed(3)}, ${matrix[13].toStringAsFixed(3)}\n' + '${matrix[2].toStringAsFixed(3)}, ${matrix[6].toStringAsFixed(3)}, ${matrix[10].toStringAsFixed(3)}, ${matrix[14].toStringAsFixed(3)}\n' + '${matrix[3].toStringAsFixed(3)}, ${matrix[7].toStringAsFixed(3)}, ${matrix[11].toStringAsFixed(3)}, ${matrix[15].toStringAsFixed(3)}\n'; } /// Roughly the inverse of [ui.Shadow.convertRadiusToSigma]. diff --git a/lib/web_ui/lib/src/engine/window.dart b/lib/web_ui/lib/src/engine/window.dart index 94d73f6c7d717..19597f6ae32de 100644 --- a/lib/web_ui/lib/src/engine/window.dart +++ b/lib/web_ui/lib/src/engine/window.dart @@ -13,20 +13,33 @@ const bool _debugPrintPlatformMessages = false; /// This may be overridden in tests, for example, to pump fake frames. ui.VoidCallback? scheduleFrameCallback; +typedef _JsSetUrlStrategy = void Function(JsUrlStrategy?); + +/// A JavaScript hook to customize the URL strategy of a Flutter app. +// +// Keep this js name in sync with flutter_web_plugins. Find it at: +// https://github.com/flutter/flutter/blob/custom_location_strategy/packages/flutter_web_plugins/lib/src/navigation/js_url_strategy.dart +// +// TODO: Add integration test https://github.com/flutter/flutter/issues/66852 +@JS('_flutter_web_set_location_strategy') +external set _jsSetUrlStrategy(_JsSetUrlStrategy? newJsSetUrlStrategy); + +UrlStrategy? _createDefaultUrlStrategy() { + return ui.debugEmulateFlutterTesterEnvironment + ? null + : const HashUrlStrategy(); +} + /// The Web implementation of [ui.Window]. class EngineWindow extends ui.Window { EngineWindow() { _addBrightnessMediaQueryListener(); - js.context['_flutter_web_set_location_strategy'] = (LocationStrategy strategy) { - locationStrategy = strategy; - }; - registerHotRestartListener(() { - js.context['_flutter_web_set_location_strategy'] = null; - }); + _addUrlStrategyListener(); } @override - double get devicePixelRatio => _debugDevicePixelRatio ?? browserDevicePixelRatio; + double get devicePixelRatio => + _debugDevicePixelRatio ?? browserDevicePixelRatio; /// Returns device pixel ratio returned by browser. static double get browserDevicePixelRatio { @@ -117,7 +130,8 @@ class EngineWindow extends ui.Window { double height = 0; double width = 0; if (html.window.visualViewport != null) { - height = html.window.visualViewport!.height!.toDouble() * devicePixelRatio; + height = + html.window.visualViewport!.height!.toDouble() * devicePixelRatio; width = html.window.visualViewport!.width!.toDouble() * devicePixelRatio; } else { height = html.window.innerHeight! * devicePixelRatio; @@ -126,7 +140,7 @@ class EngineWindow extends ui.Window { // This method compares the new dimensions with the previous ones. // Return false if the previous dimensions are not set. - if(_physicalSize != null) { + if (_physicalSize != null) { // First confirm both height and width are effected. if (_physicalSize!.height != height && _physicalSize!.width != width) { // If prior to rotation height is bigger than width it should be the @@ -154,78 +168,41 @@ class EngineWindow extends ui.Window { /// Handles the browser history integration to allow users to use the back /// button, etc. @visibleForTesting - BrowserHistory get browserHistory => _browserHistory; - BrowserHistory _browserHistory = MultiEntriesBrowserHistory(); - - @visibleForTesting - Future debugSwitchBrowserHistory({required bool useSingle}) async { - if (useSingle) - await _useSingleEntryBrowserHistory(); - else - await _useMultiEntryBrowserHistory(); - } - - /// This function should only be used for test setup. In real application, we - /// only allow one time switch from the MultiEntriesBrowserHistory to - /// the SingleEntryBrowserHistory to prevent the application to switch back - /// forth between router and non-router. - Future _useMultiEntryBrowserHistory() async { - if (_browserHistory is MultiEntriesBrowserHistory) { - return; - } - final LocationStrategy? strategy = _browserHistory.locationStrategy; - if (strategy != null) - await _browserHistory.setLocationStrategy(null); - _browserHistory = MultiEntriesBrowserHistory(); - if (strategy != null) - await _browserHistory.setLocationStrategy(strategy); + BrowserHistory get browserHistory { + return _browserHistory ??= + MultiEntriesBrowserHistory(urlStrategy: _createDefaultUrlStrategy()); } + BrowserHistory? _browserHistory; + Future _useSingleEntryBrowserHistory() async { if (_browserHistory is SingleEntryBrowserHistory) { return; } - final LocationStrategy? strategy = _browserHistory.locationStrategy; - if (strategy != null) - await _browserHistory.setLocationStrategy(null); - _browserHistory = SingleEntryBrowserHistory(); - if (strategy != null) - await _browserHistory.setLocationStrategy(strategy); + final UrlStrategy? strategy = _browserHistory?.urlStrategy; + await _browserHistory?.tearDown(); + _browserHistory = SingleEntryBrowserHistory(urlStrategy: strategy); } - /// Simulates clicking the browser's back button. - Future webOnlyBack() => _browserHistory.back(); - /// Lazily initialized when the `defaultRouteName` getter is invoked. /// - /// The reason for the lazy initialization is to give enough time for the app to set [locationStrategy] + /// The reason for the lazy initialization is to give enough time for the app to set [urlStrategy] /// in `lib/src/ui/initialization.dart`. String? _defaultRouteName; @override - String get defaultRouteName => _defaultRouteName ??= _browserHistory.currentPath; + String get defaultRouteName { + return _defaultRouteName ??= browserHistory.currentPath; + } @override void scheduleFrame() { if (scheduleFrameCallback == null) { - throw new Exception( - 'scheduleFrameCallback must be initialized first.'); + throw new Exception('scheduleFrameCallback must be initialized first.'); } scheduleFrameCallback!(); } - /// Change the strategy to use for handling browser history location. - /// Setting this member will automatically update [_browserHistory]. - /// - /// By setting this to null, the browser history will be disabled. - set locationStrategy(LocationStrategy? strategy) { - _browserHistory.setLocationStrategy(strategy); - } - - /// Returns the currently active location strategy. - @visibleForTesting - LocationStrategy? get locationStrategy => _browserHistory.locationStrategy; - @override ui.VoidCallback? get onTextScaleFactorChanged => _onTextScaleFactorChanged; ui.VoidCallback? _onTextScaleFactorChanged; @@ -477,8 +454,8 @@ class EngineWindow extends ui.Window { /// Engine code should use this method instead of the callback directly. /// Otherwise zones won't work properly. - void invokeOnPlatformMessage( - String name, ByteData? data, ui.PlatformMessageResponseCallback callback) { + void invokeOnPlatformMessage(String name, ByteData? data, + ui.PlatformMessageResponseCallback callback) { _invoke3( _onPlatformMessage, _onPlatformMessageZone, @@ -500,7 +477,9 @@ class EngineWindow extends ui.Window { /// Wraps the given [callback] in another callback that ensures that the /// original callback is called in the zone it was registered in. - static ui.PlatformMessageResponseCallback? _zonedPlatformMessageResponseCallback(ui.PlatformMessageResponseCallback? callback) { + static ui.PlatformMessageResponseCallback? + _zonedPlatformMessageResponseCallback( + ui.PlatformMessageResponseCallback? callback) { if (callback == null) { return null; } @@ -564,7 +543,7 @@ class EngineWindow extends ui.Window { final MethodCall decoded = codec.decodeMethodCall(data); switch (decoded.method) { case 'SystemNavigator.pop': - _browserHistory.exit().then((_) { + browserHistory.exit().then((_) { _replyToPlatformMessage( callback, codec.encodeSuccessEnvelope(true)); }); @@ -585,8 +564,8 @@ class EngineWindow extends ui.Window { case 'SystemChrome.setPreferredOrientations': final List? arguments = decoded.arguments; domRenderer.setPreferredOrientation(arguments).then((bool success) { - _replyToPlatformMessage(callback, - codec.encodeSuccessEnvelope(success)); + _replyToPlatformMessage( + callback, codec.encodeSuccessEnvelope(success)); }); return; case 'SystemSound.play': @@ -632,7 +611,8 @@ class EngineWindow extends ui.Window { case 'flutter/platform_views': if (experimentalUseSkia) { - rasterizer!.surface.viewEmbedder.handlePlatformViewCall(data, callback); + rasterizer!.surface.viewEmbedder + .handlePlatformViewCall(data, callback); } else { ui.handlePlatformViewCall(data!, callback!); } @@ -646,27 +626,11 @@ class EngineWindow extends ui.Window { return; case 'flutter/navigation': - const MethodCodec codec = JSONMethodCodec(); - final MethodCall decoded = codec.decodeMethodCall(data); - final Map message = decoded.arguments as Map; - switch (decoded.method) { - case 'routeUpdated': - _useSingleEntryBrowserHistory().then((void data) { - _browserHistory.setRouteName(message['routeName']); - _replyToPlatformMessage( - callback, codec.encodeSuccessEnvelope(true)); - }); - break; - case 'routeInformationUpdated': - assert(_browserHistory is MultiEntriesBrowserHistory); - _browserHistory.setRouteName( - message['location'], - state: message['state'], - ); - _replyToPlatformMessage( - callback, codec.encodeSuccessEnvelope(true)); - break; - } + _handleNavigationMessage(data, callback).then((handled) { + if (!handled && callback != null) { + callback(null); + } + }); // As soon as Flutter starts taking control of the app navigation, we // should reset [_defaultRouteName] to "/" so it doesn't have any // further effect after this point. @@ -685,6 +649,51 @@ class EngineWindow extends ui.Window { _replyToPlatformMessage(callback, null); } + @visibleForTesting + Future debugInitializeHistory( + UrlStrategy? strategy, { + required bool useSingle, + }) async { + await _browserHistory?.tearDown(); + if (useSingle) { + _browserHistory = SingleEntryBrowserHistory(urlStrategy: strategy); + } else { + _browserHistory = MultiEntriesBrowserHistory(urlStrategy: strategy); + } + } + + @visibleForTesting + Future debugResetHistory() async { + await _browserHistory?.tearDown(); + _browserHistory = null; + } + + Future _handleNavigationMessage( + ByteData? data, + ui.PlatformMessageResponseCallback? callback, + ) async { + const MethodCodec codec = JSONMethodCodec(); + final MethodCall decoded = codec.decodeMethodCall(data); + final Map arguments = decoded.arguments; + + switch (decoded.method) { + case 'routeUpdated': + await _useSingleEntryBrowserHistory(); + browserHistory.setRouteName(arguments['routeName']); + _replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true)); + return true; + case 'routeInformationUpdated': + assert(browserHistory is MultiEntriesBrowserHistory); + browserHistory.setRouteName( + arguments['location'], + state: arguments['state'], + ); + _replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true)); + return true; + } + return false; + } + int _getHapticFeedbackDuration(String? type) { switch (type) { case 'HapticFeedbackType.lightImpact': @@ -746,7 +755,8 @@ class EngineWindow extends ui.Window { : ui.Brightness.light); _brightnessMediaQueryListener = (html.Event event) { - final html.MediaQueryListEvent mqEvent = event as html.MediaQueryListEvent; + final html.MediaQueryListEvent mqEvent = + event as html.MediaQueryListEvent; _updatePlatformBrightness( mqEvent.matches! ? ui.Brightness.dark : ui.Brightness.light); }; @@ -756,6 +766,21 @@ class EngineWindow extends ui.Window { }); } + void _addUrlStrategyListener() { + _jsSetUrlStrategy = allowInterop((JsUrlStrategy? jsStrategy) { + assert( + _browserHistory == null, + 'Cannot set URL strategy more than once.', + ); + final UrlStrategy? strategy = + jsStrategy == null ? null : CustomUrlStrategy.fromJs(jsStrategy); + _browserHistory = MultiEntriesBrowserHistory(urlStrategy: strategy); + }); + registerHotRestartListener(() { + _jsSetUrlStrategy = null; + }); + } + /// Remove the callback function for listening changes in [_brightnessMediaQuery] value. void _removeBrightnessMediaQueryListener() { _brightnessMediaQuery.removeListener(_brightnessMediaQueryListener); @@ -785,7 +810,8 @@ class EngineWindow extends ui.Window { } @visibleForTesting - late Rasterizer? rasterizer = experimentalUseSkia ? Rasterizer(Surface(HtmlViewEmbedder())) : null; + late Rasterizer? rasterizer = + experimentalUseSkia ? Rasterizer(Surface(HtmlViewEmbedder())) : null; } bool _handleWebTestEnd2EndMessage(MethodCodec codec, ByteData? data) { @@ -831,8 +857,8 @@ void _invoke1(void callback(A a)?, Zone? zone, A arg) { } /// Invokes [callback] inside the given [zone] passing it [arg1], [arg2], and [arg3]. -void _invoke3( - void callback(A1 a1, A2 a2, A3 a3)?, Zone? zone, A1 arg1, A2 arg2, A3 arg3) { +void _invoke3(void callback(A1 a1, A2 a2, A3 a3)?, Zone? zone, + A1 arg1, A2 arg2, A3 arg3) { if (callback == null) { return; } diff --git a/lib/web_ui/lib/src/ui/initialization.dart b/lib/web_ui/lib/src/ui/initialization.dart index a7b06b3586def..ca317304ec79b 100644 --- a/lib/web_ui/lib/src/ui/initialization.dart +++ b/lib/web_ui/lib/src/ui/initialization.dart @@ -21,10 +21,6 @@ Future webOnlyInitializePlatform({ Future _initializePlatform({ engine.AssetManager? assetManager, }) async { - if (!debugEmulateFlutterTesterEnvironment) { - engine.window.locationStrategy = const engine.HashLocationStrategy(); - } - engine.initializeEngine(); // This needs to be after `webOnlyInitializeEngine` because that is where the diff --git a/lib/web_ui/pubspec.yaml b/lib/web_ui/pubspec.yaml index 84e6d33eccde1..c761816991502 100644 --- a/lib/web_ui/pubspec.yaml +++ b/lib/web_ui/pubspec.yaml @@ -14,7 +14,7 @@ dev_dependencies: image: 2.1.13 js: 0.6.1+1 mockito: 4.1.1 - path: 1.7.0 + path: 1.8.0-nullsafety.1 test: 1.14.3 quiver: 2.1.3 build_resolvers: 1.3.10 @@ -23,6 +23,8 @@ dev_dependencies: build_web_compilers: 2.11.0 yaml: 2.2.1 watcher: 0.9.7+15 + web_test_utils: + path: ../../web_sdk/web_test_utils web_engine_tester: path: ../../web_sdk/web_engine_tester simulators: @@ -34,4 +36,4 @@ dev_dependencies: git: url: git://github.com/flutter/web_installers.git path: packages/web_drivers/ - ref: 1cea0d79cad1ebc217c4bcbeba1be41470674a49 + ref: 58081a8b2fbf234e9c8da86fce28adfefe3c2093 diff --git a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart index b03f3526db149..28e1c11198468 100644 --- a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart +++ b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart @@ -51,7 +51,7 @@ void testMain() { _toSkPointTests(); _toSkColorStopsTests(); _toSkMatrixFromFloat32Tests(); - _skSkRectTests(); + _toSkRectTests(); _skVerticesTests(); group('SkPath', () { _pathTests(); @@ -59,7 +59,10 @@ void testMain() { group('SkCanvas', () { _canvasTests(); }); - // TODO: https://github.com/flutter/flutter/issues/60040 + group('SkParagraph', () { + _textStyleTests(); + }); + // TODO: https://github.com/flutter/flutter/issues/60040 }, skip: isIosSafari); } @@ -203,11 +206,13 @@ void _fillTypeTests() { void _pathOpTests() { test('path op mapping is correct', () { - expect(canvasKit.PathOp.Difference.value, ui.PathOperation.difference.index); + expect( + canvasKit.PathOp.Difference.value, ui.PathOperation.difference.index); expect(canvasKit.PathOp.Intersect.value, ui.PathOperation.intersect.index); expect(canvasKit.PathOp.Union.value, ui.PathOperation.union.index); expect(canvasKit.PathOp.XOR.value, ui.PathOperation.xor.index); - expect(canvasKit.PathOp.ReverseDifference.value, ui.PathOperation.reverseDifference.index); + expect(canvasKit.PathOp.ReverseDifference.value, + ui.PathOperation.reverseDifference.index); }); test('ui.PathOperation converts to SkPathOp', () { @@ -269,8 +274,10 @@ void _pointModeTests() { void _vertexModeTests() { test('vertex mode mapping is correct', () { expect(canvasKit.VertexMode.Triangles.value, ui.VertexMode.triangles.index); - expect(canvasKit.VertexMode.TrianglesStrip.value, ui.VertexMode.triangleStrip.index); - expect(canvasKit.VertexMode.TriangleFan.value, ui.VertexMode.triangleFan.index); + expect(canvasKit.VertexMode.TrianglesStrip.value, + ui.VertexMode.triangleStrip.index); + expect(canvasKit.VertexMode.TriangleFan.value, + ui.VertexMode.triangleFan.index); }); test('ui.VertexMode converts to SkVertexMode', () { @@ -282,7 +289,8 @@ void _vertexModeTests() { void _imageTests() { test('MakeAnimatedImageFromEncoded makes a non-animated image', () { - final SkAnimatedImage nonAnimated = canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage); + final SkAnimatedImage nonAnimated = + canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage); expect(nonAnimated.getFrameCount(), 1); expect(nonAnimated.getRepetitionCount(), 0); expect(nonAnimated.width(), 1); @@ -304,9 +312,10 @@ void _imageTests() { }); test('MakeAnimatedImageFromEncoded makes an animated image', () { - final SkAnimatedImage animated = canvasKit.MakeAnimatedImageFromEncoded(kAnimatedGif); + final SkAnimatedImage animated = + canvasKit.MakeAnimatedImageFromEncoded(kAnimatedGif); expect(animated.getFrameCount(), 3); - expect(animated.getRepetitionCount(), -1); // animates forever + expect(animated.getRepetitionCount(), -1); // animates forever expect(animated.width(), 1); expect(animated.height(), 1); for (int i = 0; i < 100; i++) { @@ -324,35 +333,39 @@ void _shaderTests() { }); test('MakeRadialGradient', () { - expect(canvasKit.SkShader.MakeRadialGradient( - Float32List.fromList([1, 1]), - 10.0, - [ - Float32List.fromList([0, 0, 0, 1]), - Float32List.fromList([1, 1, 1, 1]), - ], - Float32List.fromList([0, 1]), - canvasKit.TileMode.Repeat, - toSkMatrixFromFloat32(Matrix4.identity().storage), - 0, - ), isNotNull); + expect( + canvasKit.SkShader.MakeRadialGradient( + Float32List.fromList([1, 1]), + 10.0, + [ + Float32List.fromList([0, 0, 0, 1]), + Float32List.fromList([1, 1, 1, 1]), + ], + Float32List.fromList([0, 1]), + canvasKit.TileMode.Repeat, + toSkMatrixFromFloat32(Matrix4.identity().storage), + 0, + ), + isNotNull); }); test('MakeTwoPointConicalGradient', () { - expect(canvasKit.SkShader.MakeTwoPointConicalGradient( - Float32List.fromList([1, 1]), - 10.0, - Float32List.fromList([1, 1]), - 10.0, - [ - Float32List.fromList([0, 0, 0, 1]), - Float32List.fromList([1, 1, 1, 1]), - ], - Float32List.fromList([0, 1]), - canvasKit.TileMode.Repeat, - toSkMatrixFromFloat32(Matrix4.identity().storage), - 0, - ), isNotNull); + expect( + canvasKit.SkShader.MakeTwoPointConicalGradient( + Float32List.fromList([1, 1]), + 10.0, + Float32List.fromList([1, 1]), + 10.0, + [ + Float32List.fromList([0, 0, 0, 1]), + Float32List.fromList([1, 1, 1, 1]), + ], + Float32List.fromList([0, 1]), + canvasKit.TileMode.Repeat, + toSkMatrixFromFloat32(Matrix4.identity().storage), + 0, + ), + isNotNull); }); } @@ -398,11 +411,13 @@ void _paintTests() { void _maskFilterTests() { test('MakeBlurMaskFilter', () { - expect(canvasKit.MakeBlurMaskFilter( - canvasKit.BlurStyle.Outer, - 5.0, - false, - ), isNotNull); + expect( + canvasKit.MakeBlurMaskFilter( + canvasKit.BlurStyle.Outer, + 5.0, + false, + ), + isNotNull); }); } @@ -543,18 +558,44 @@ void _toSkMatrixFromFloat32Tests() { ..translate(1, 2, 3) ..rotateZ(4); expect( - toSkMatrixFromFloat32(matrix.storage), - Float32List.fromList([ - -0.6536436080932617, - 0.756802499294281, + toSkMatrixFromFloat32(matrix.storage), + Float32List.fromList([ + -0.6536436080932617, + 0.756802499294281, + 1, + -0.756802499294281, + -0.6536436080932617, + 2, + -0.0, + 0, + 1, + ])); + }); +} + +void _toSkRectTests() { + test('toSkRect', () { + expect(toSkRect(ui.Rect.fromLTRB(1, 2, 3, 4)), [1, 2, 3, 4]); + }); + + test('fromSkRect', () { + expect(fromSkRect(Float32List.fromList([1, 2, 3, 4])), + ui.Rect.fromLTRB(1, 2, 3, 4)); + }); + + test('toSkRRect', () { + expect( + toSkRRect(ui.RRect.fromLTRBAndCorners( 1, - -0.756802499294281, - -0.6536436080932617, 2, - -0.0, - 0, - 1, - ]) + 3, + 4, + topLeft: ui.Radius.elliptical(5, 6), + topRight: ui.Radius.elliptical(7, 8), + bottomRight: ui.Radius.elliptical(9, 10), + bottomLeft: ui.Radius.elliptical(11, 12), + )), + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], ); }); } @@ -581,7 +622,7 @@ void _pathTests() { test('addArc', () { path.addArc( - SkRect(fLeft: 10, fTop: 20, fRight: 30, fBottom: 40), + toSkRect(ui.Rect.fromLTRB(10, 20, 30, 40)), 1, 5, ); @@ -589,7 +630,7 @@ void _pathTests() { test('addOval', () { path.addOval( - SkRect(fLeft: 10, fTop: 20, fRight: 30, fBottom: 40), + toSkRect(ui.Rect.fromLTRB(10, 20, 30, 40)), false, 1, ); @@ -608,36 +649,24 @@ void _pathTests() { freeFloat32List(encodedPoints); }); - test('addRoundRect', () { + test('addRRect', () { final ui.RRect rrect = ui.RRect.fromRectAndRadius( ui.Rect.fromLTRB(10, 10, 20, 20), ui.Radius.circular(3), ); - final SkFloat32List skRadii = mallocFloat32List(8); - final Float32List radii = skRadii.toTypedArray(); - radii[0] = rrect.tlRadiusX; - radii[1] = rrect.tlRadiusY; - radii[2] = rrect.trRadiusX; - radii[3] = rrect.trRadiusY; - radii[4] = rrect.brRadiusX; - radii[5] = rrect.brRadiusY; - radii[6] = rrect.blRadiusX; - radii[7] = rrect.blRadiusY; - path.addRoundRect( - toOuterSkRect(rrect), - radii, + path.addRRect( + toSkRRect(rrect), false, ); - freeFloat32List(skRadii); }); test('addRect', () { - path.addRect(SkRect(fLeft: 1, fTop: 2, fRight: 3, fBottom: 4)); + path.addRect(toSkRect(ui.Rect.fromLTRB(1, 2, 3, 4))); }); test('arcTo', () { path.arcToOval( - SkRect(fLeft: 1, fTop: 2, fRight: 3, fBottom: 4), + toSkRect(ui.Rect.fromLTRB(1, 2, 3, 4)), 5, 40, false, @@ -676,7 +705,7 @@ void _pathTests() { test('getBounds', () { final SkPath testPath = _testClosedSkPath(); - final ui.Rect bounds = testPath.getBounds().toRect(); + final ui.Rect bounds = fromSkRect(testPath.getBounds()); expect(bounds, const ui.Rect.fromLTRB(10, 10, 20, 20)); }); @@ -726,13 +755,15 @@ void _pathTests() { test('reset', () { final SkPath testPath = _testClosedSkPath(); - expect(testPath.getBounds().toRect(), const ui.Rect.fromLTRB(10, 10, 20, 20)); + expect(fromSkRect(testPath.getBounds()), + const ui.Rect.fromLTRB(10, 10, 20, 20)); testPath.reset(); - expect(testPath.getBounds().toRect(), ui.Rect.zero); + expect(fromSkRect(testPath.getBounds()), ui.Rect.zero); }); test('toSVGString', () { - expect(_testClosedSkPath().toSVGString(), 'M10 10L20 10L20 20L10 20L10 10Z'); + expect( + _testClosedSkPath().toSVGString(), 'M10 10L20 10L20 20L10 20L10 10Z'); }); test('isEmpty', () { @@ -743,22 +774,24 @@ void _pathTests() { test('copy', () { final SkPath original = _testClosedSkPath(); final SkPath copy = original.copy(); - expect(original.getBounds().toRect(), copy.getBounds().toRect()); + expect(fromSkRect(original.getBounds()), fromSkRect(copy.getBounds())); }); test('transform', () { path = _testClosedSkPath(); path.transform(2, 0, 10, 0, 2, 10, 0, 0, 0); - final ui.Rect transformedBounds = path.getBounds().toRect(); + final ui.Rect transformedBounds = fromSkRect(path.getBounds()); expect(transformedBounds, ui.Rect.fromLTRB(30, 30, 50, 50)); }); test('SkContourMeasureIter/SkContourMeasure', () { - final SkContourMeasureIter iter = SkContourMeasureIter(_testClosedSkPath(), false, 0); + final SkContourMeasureIter iter = + SkContourMeasureIter(_testClosedSkPath(), false, 0); final SkContourMeasure measure1 = iter.next(); expect(measure1.length(), 40); expect(measure1.getPosTan(5), Float32List.fromList([15, 10, 1, 0])); - expect(measure1.getPosTan(15), Float32List.fromList([20, 15, 0, 1])); + expect( + measure1.getPosTan(15), Float32List.fromList([20, 15, 0, 1])); expect(measure1.isClosed(), true); // Starting with a box path: @@ -783,29 +816,13 @@ void _pathTests() { // | | // 20 +-----------+ final SkPath segment = measure1.getSegment(5, 15, true); - expect(segment.getBounds().toRect(), ui.Rect.fromLTRB(15, 10, 20, 15)); + expect(fromSkRect(segment.getBounds()), ui.Rect.fromLTRB(15, 10, 20, 15)); final SkContourMeasure measure2 = iter.next(); expect(measure2, isNull); }); } -void _skSkRectTests() { - test('SkRect', () { - final SkRect rect = SkRect(fLeft: 1, fTop: 2, fRight: 3, fBottom: 4); - expect(rect.fLeft, 1); - expect(rect.fTop, 2); - expect(rect.fRight, 3); - expect(rect.fBottom, 4); - - final ui.Rect uiRect = rect.toRect(); - expect(uiRect.left, 1); - expect(uiRect.top, 2); - expect(uiRect.right, 3); - expect(uiRect.bottom, 4); - }); -} - SkVertices _testVertices() { return canvasKit.MakeSkVertices( canvasKit.VertexMode.Triangles, @@ -840,12 +857,8 @@ void _canvasTests() { setUp(() { recorder = SkPictureRecorder(); - canvas = recorder.beginRecording(SkRect( - fLeft: 0, - fTop: 0, - fRight: 100, - fBottom: 100, - )); + canvas = + recorder.beginRecording(toSkRect(ui.Rect.fromLTRB(0, 0, 100, 100))); }); tearDown(() { @@ -866,33 +879,23 @@ void _canvasTests() { test('saveLayer', () { canvas.saveLayer( - SkRect( - fLeft: 0, - fTop: 0, - fRight: 100, - fBottom: 100, - ), SkPaint(), + toSkRect(ui.Rect.fromLTRB(0, 0, 100, 100)), + null, + null, ); }); - test('SkCanvasSaveLayerWithoutBoundsOverload.saveLayer', () { - final SkCanvasSaveLayerWithoutBoundsOverload override = canvas as SkCanvasSaveLayerWithoutBoundsOverload; - override.saveLayer(SkPaint()); + test('saveLayer without bounds', () { + canvas.saveLayer(SkPaint(), null, null, null); }); - test('SkCanvasSaveLayerWithFilterOverload.saveLayer', () { - final SkCanvasSaveLayerWithFilterOverload override = canvas as SkCanvasSaveLayerWithFilterOverload; - override.saveLayer( + test('saveLayer with filter', () { + canvas.saveLayer( SkPaint(), + toSkRect(ui.Rect.fromLTRB(0, 0, 100, 100)), canvasKit.SkImageFilter.MakeBlur(1, 2, canvasKit.TileMode.Repeat, null), 0, - SkRect( - fLeft: 0, - fTop: 0, - fRight: 100, - fBottom: 100, - ), ); }); @@ -910,22 +913,7 @@ void _canvasTests() { test('clipRRect', () { canvas.clipRRect( - SkRRect( - rect: SkRect( - fLeft: 0, - fTop: 0, - fRight: 100, - fBottom: 100, - ), - rx1: 1, - ry1: 2, - rx2: 3, - ry2: 4, - rx3: 5, - ry3: 6, - rx4: 7, - ry4: 8, - ), + Float32List.fromList([0, 0, 100, 100, 1, 2, 3, 4, 5, 6, 7, 8]), canvasKit.ClipOp.Intersect, true, ); @@ -933,12 +921,7 @@ void _canvasTests() { test('clipRect', () { canvas.clipRect( - SkRect( - fLeft: 0, - fTop: 0, - fRight: 100, - fBottom: 100, - ), + Float32List.fromList([0, 0, 100, 100]), canvasKit.ClipOp.Intersect, true, ); @@ -946,12 +929,7 @@ void _canvasTests() { test('drawArc', () { canvas.drawArc( - SkRect( - fLeft: 0, - fTop: 0, - fRight: 100, - fBottom: 50, - ), + Float32List.fromList([0, 0, 100, 50]), 0, 100, true, @@ -960,7 +938,8 @@ void _canvasTests() { }); test('drawAtlas', () { - final SkAnimatedImage image = canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage); + final SkAnimatedImage image = + canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage); canvas.drawAtlas( image.getCurrentFrame(), Float32List.fromList([0, 0, 1, 1]), @@ -984,44 +963,15 @@ void _canvasTests() { test('drawDRRect', () { canvas.drawDRRect( - SkRRect( - rect: SkRect( - fLeft: 0, - fTop: 0, - fRight: 100, - fBottom: 100, - ), - rx1: 1, - ry1: 2, - rx2: 3, - ry2: 4, - rx3: 5, - ry3: 6, - rx4: 7, - ry4: 8, - ), - SkRRect( - rect: SkRect( - fLeft: 20, - fTop: 20, - fRight: 80, - fBottom: 80, - ), - rx1: 1, - ry1: 2, - rx2: 3, - ry2: 4, - rx3: 5, - ry3: 6, - rx4: 7, - ry4: 8, - ), + Float32List.fromList([0, 0, 100, 100, 1, 2, 3, 4, 5, 6, 7, 8]), + Float32List.fromList([20, 20, 80, 80, 1, 2, 3, 4, 5, 6, 7, 8]), SkPaint(), ); }); test('drawImage', () { - final SkAnimatedImage image = canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage); + final SkAnimatedImage image = + canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage); canvas.drawImage( image.getCurrentFrame(), 10, @@ -1031,22 +981,24 @@ void _canvasTests() { }); test('drawImageRect', () { - final SkAnimatedImage image = canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage); + final SkAnimatedImage image = + canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage); canvas.drawImageRect( image.getCurrentFrame(), - SkRect(fLeft: 0, fTop: 0, fRight: 1, fBottom: 1), - SkRect(fLeft: 0, fTop: 0, fRight: 1, fBottom: 1), + Float32List.fromList([0, 0, 1, 1]), + Float32List.fromList([0, 0, 1, 1]), SkPaint(), false, ); }); test('drawImageNine', () { - final SkAnimatedImage image = canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage); + final SkAnimatedImage image = + canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage); canvas.drawImageNine( image.getCurrentFrame(), - SkRect(fLeft: 0, fTop: 0, fRight: 1, fBottom: 1), - SkRect(fLeft: 0, fTop: 0, fRight: 1, fBottom: 1), + Float32List.fromList([0, 0, 1, 1]), + Float32List.fromList([0, 0, 1, 1]), SkPaint(), ); }); @@ -1056,7 +1008,7 @@ void _canvasTests() { }); test('drawOval', () { - canvas.drawOval(SkRect(fLeft: 0, fTop: 0, fRight: 1, fBottom: 1), SkPaint()); + canvas.drawOval(Float32List.fromList([0, 0, 1, 1]), SkPaint()); }); test('drawPaint', () { @@ -1080,34 +1032,14 @@ void _canvasTests() { test('drawRRect', () { canvas.drawRRect( - SkRRect( - rect: SkRect( - fLeft: 0, - fTop: 0, - fRight: 100, - fBottom: 100, - ), - rx1: 1, - ry1: 2, - rx2: 3, - ry2: 4, - rx3: 5, - ry3: 6, - rx4: 7, - ry4: 8, - ), + Float32List.fromList([0, 0, 100, 100, 1, 2, 3, 4, 5, 6, 7, 8]), SkPaint(), ); }); test('drawRect', () { canvas.drawRect( - SkRect( - fLeft: 0, - fTop: 0, - fRight: 100, - fBottom: 100, - ), + Float32List.fromList([0, 0, 100, 100]), SkPaint(), ); }); @@ -1120,12 +1052,13 @@ void _canvasTests() { const double spotAlpha = 0.25; final SkPath path = _testClosedSkPath(); - final ui.Rect bounds = path.getBounds().toRect(); + final ui.Rect bounds = fromSkRect(path.getBounds()); final double shadowX = (bounds.left + bounds.right) / 2.0; final double shadowY = bounds.top - 600.0; const ui.Color color = ui.Color(0xAABBCCDD); - ui.Color inAmbient = color.withAlpha((color.alpha * ambientAlpha).round()); + ui.Color inAmbient = + color.withAlpha((color.alpha * ambientAlpha).round()); ui.Color inSpot = color.withAlpha((color.alpha * spotAlpha).round()); final SkTonalColors inTonalColors = SkTonalColors( @@ -1138,8 +1071,7 @@ void _canvasTests() { canvas.drawShadow( path, - Float32List(3) - ..[2] = devicePixelRatio * elevation, + Float32List(3)..[2] = devicePixelRatio * elevation, Float32List(3) ..[0] = shadowX ..[1] = shadowY @@ -1186,12 +1118,8 @@ void _canvasTests() { test('drawPicture', () { final SkPictureRecorder otherRecorder = SkPictureRecorder(); - final SkCanvas otherCanvas = otherRecorder.beginRecording(SkRect( - fLeft: 0, - fTop: 0, - fRight: 100, - fBottom: 100, - )); + final SkCanvas otherCanvas = + otherRecorder.beginRecording(Float32List.fromList([0, 0, 100, 100])); otherCanvas.drawLine(0, 0, 10, 10, SkPaint()); canvas.drawPicture(otherRecorder.finishRecordingAsPicture()); }); @@ -1212,26 +1140,79 @@ void _canvasTests() { test('toImage.toByteData', () async { final SkPictureRecorder otherRecorder = SkPictureRecorder(); - final SkCanvas otherCanvas = otherRecorder.beginRecording(SkRect( - fLeft: 0, - fTop: 0, - fRight: 1, - fBottom: 1, - )); + final SkCanvas otherCanvas = + otherRecorder.beginRecording(Float32List.fromList([0, 0, 1, 1])); otherCanvas.drawRect( - SkRect( - fLeft: 0, - fTop: 0, - fRight: 1, - fBottom: 1, - ), + Float32List.fromList([0, 0, 1, 1]), SkPaint(), ); - final CkPicture picture = CkPicture(otherRecorder.finishRecordingAsPicture(), null); + final CkPicture picture = + CkPicture(otherRecorder.finishRecordingAsPicture(), null); final CkImage image = await picture.toImage(1, 1); - final ByteData rawData = await image.toByteData(format: ui.ImageByteFormat.rawRgba); + final ByteData rawData = + await image.toByteData(format: ui.ImageByteFormat.rawRgba); expect(rawData, isNotNull); - final ByteData pngData = await image.toByteData(format: ui.ImageByteFormat.png); + final ByteData pngData = + await image.toByteData(format: ui.ImageByteFormat.png); expect(pngData, isNotNull); }); } + +void _textStyleTests() { + test('SkTextDecorationStyle mapping is correct', () { + expect(canvasKit.DecorationStyle.Solid.value, + ui.TextDecorationStyle.solid.index); + expect(canvasKit.DecorationStyle.Double.value, + ui.TextDecorationStyle.double.index); + expect(canvasKit.DecorationStyle.Dotted.value, + ui.TextDecorationStyle.dotted.index); + expect(canvasKit.DecorationStyle.Dashed.value, + ui.TextDecorationStyle.dashed.index); + expect(canvasKit.DecorationStyle.Wavy.value, + ui.TextDecorationStyle.wavy.index); + }); + + test('ui.TextDecorationStyle converts to SkTextDecorationStyle', () { + for (ui.TextDecorationStyle decorationStyle + in ui.TextDecorationStyle.values) { + expect(toSkTextDecorationStyle(decorationStyle).value, + decorationStyle.index); + } + }); + + test('SkTextBaseline mapping is correct', () { + expect(canvasKit.TextBaseline.Alphabetic.value, + ui.TextBaseline.alphabetic.index); + expect(canvasKit.TextBaseline.Ideographic.value, + ui.TextBaseline.ideographic.index); + }); + + test('ui.TextBaseline converts to SkTextBaseline', () { + for (ui.TextBaseline textBaseline in ui.TextBaseline.values) { + expect(toSkTextBaseline(textBaseline).value, textBaseline.index); + } + }); + + test('SkPlaceholderAlignment mapping is correct', () { + expect(canvasKit.PlaceholderAlignment.Baseline.value, + ui.PlaceholderAlignment.baseline.index); + expect(canvasKit.PlaceholderAlignment.AboveBaseline.value, + ui.PlaceholderAlignment.aboveBaseline.index); + expect(canvasKit.PlaceholderAlignment.BelowBaseline.value, + ui.PlaceholderAlignment.belowBaseline.index); + expect(canvasKit.PlaceholderAlignment.Top.value, + ui.PlaceholderAlignment.top.index); + expect(canvasKit.PlaceholderAlignment.Bottom.value, + ui.PlaceholderAlignment.bottom.index); + expect(canvasKit.PlaceholderAlignment.Middle.value, + ui.PlaceholderAlignment.middle.index); + }); + + test('ui.PlaceholderAlignment converts to SkPlaceholderAlignment', () { + for (ui.PlaceholderAlignment placeholderAlignment + in ui.PlaceholderAlignment.values) { + expect(toSkPlaceholderAlignment(placeholderAlignment).value, + placeholderAlignment.index); + } + }); +} diff --git a/lib/web_ui/test/engine/history_test.dart b/lib/web_ui/test/engine/history_test.dart index 4c11ed0033636..f5be42ae61db7 100644 --- a/lib/web_ui/test/engine/history_test.dart +++ b/lib/web_ui/test/engine/history_test.dart @@ -16,11 +16,6 @@ import 'package:ui/src/engine.dart'; import '../spy.dart'; -TestLocationStrategy get strategy => window.browserHistory.locationStrategy; -Future setStrategy(TestLocationStrategy newStrategy) async { - await window.browserHistory.setLocationStrategy(newStrategy); -} - Map _wrapOriginState(dynamic state) { return {'origin': true, 'state': state}; } @@ -48,18 +43,19 @@ void testMain() { final PlatformMessagesSpy spy = PlatformMessagesSpy(); setUp(() async { - await window.debugSwitchBrowserHistory(useSingle: true); spy.setUp(); }); tearDown(() async { spy.tearDown(); - await setStrategy(null); + await window.debugResetHistory(); }); test('basic setup works', () async { - await setStrategy(TestLocationStrategy.fromEntry( - TestHistoryEntry('initial state', null, '/initial'))); + final TestUrlStrategy strategy = TestUrlStrategy.fromEntry( + TestHistoryEntry('initial state', null, '/initial'), + ); + await window.debugInitializeHistory(strategy, useSingle: true); // There should be two entries: origin and flutter. expect(strategy.history, hasLength(2)); @@ -82,7 +78,11 @@ void testMain() { skip: browserEngine == BrowserEngine.edge); test('browser back button pops routes correctly', () async { - await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home'))); + final TestUrlStrategy strategy = TestUrlStrategy.fromEntry( + TestHistoryEntry(null, null, '/home'), + ); + await window.debugInitializeHistory(strategy, useSingle: true); + // Initially, we should be on the flutter entry. expect(strategy.history, hasLength(2)); expect(strategy.currentEntry.state, flutterState); @@ -98,7 +98,7 @@ void testMain() { // No platform messages have been sent so far. expect(spy.messages, isEmpty); // Clicking back should take us to page1. - await strategy.back(); + await strategy.go(-1); // First, the framework should've received a `popRoute` platform message. expect(spy.messages, hasLength(1)); expect(spy.messages[0].channel, 'flutter/navigation'); @@ -115,7 +115,10 @@ void testMain() { skip: browserEngine == BrowserEngine.edge); test('multiple browser back clicks', () async { - await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home'))); + final TestUrlStrategy strategy = TestUrlStrategy.fromEntry( + TestHistoryEntry(null, null, '/home'), + ); + await window.debugInitializeHistory(strategy, useSingle: true); await routeUpdated('/page1'); await routeUpdated('/page2'); @@ -127,7 +130,7 @@ void testMain() { expect(strategy.currentEntry.url, '/page2'); // Back to page1. - await strategy.back(); + await strategy.go(-1); // 1. The engine sends a `popRoute` platform message. expect(spy.messages, hasLength(1)); expect(spy.messages[0].channel, 'flutter/navigation'); @@ -143,7 +146,7 @@ void testMain() { expect(strategy.currentEntry.url, '/page1'); // Back to home. - await strategy.back(); + await strategy.go(-1); // 1. The engine sends a `popRoute` platform message. expect(spy.messages, hasLength(1)); expect(spy.messages[0].channel, 'flutter/navigation'); @@ -161,8 +164,8 @@ void testMain() { // The next browser back will exit the app. We store the strategy locally // because it will be remove from the browser history class once it exits // the app. - TestLocationStrategy originalStrategy = strategy; - await originalStrategy.back(); + TestUrlStrategy originalStrategy = strategy; + await originalStrategy.go(-1); // 1. The engine sends a `popRoute` platform message. expect(spy.messages, hasLength(1)); expect(spy.messages[0].channel, 'flutter/navigation'); @@ -181,7 +184,10 @@ void testMain() { browserEngine == BrowserEngine.webkit); test('handle user-provided url', () async { - await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home'))); + final TestUrlStrategy strategy = TestUrlStrategy.fromEntry( + TestHistoryEntry(null, null, '/home'), + ); + await window.debugInitializeHistory(strategy, useSingle: true); await strategy.simulateUserTypingUrl('/page3'); // This delay is necessary to wait for [BrowserHistory] because it @@ -202,7 +208,7 @@ void testMain() { expect(strategy.currentEntry.url, '/page3'); // Back to home. - await strategy.back(); + await strategy.go(-1); // 1. The engine sends a `popRoute` platform message. expect(spy.messages, hasLength(1)); expect(spy.messages[0].channel, 'flutter/navigation'); @@ -221,7 +227,10 @@ void testMain() { skip: browserEngine == BrowserEngine.edge); test('user types unknown url', () async { - await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home'))); + final TestUrlStrategy strategy = TestUrlStrategy.fromEntry( + TestHistoryEntry(null, null, '/home'), + ); + await window.debugInitializeHistory(strategy, useSingle: true); await strategy.simulateUserTypingUrl('/unknown'); // This delay is necessary to wait for [BrowserHistory] because it @@ -248,18 +257,19 @@ void testMain() { final PlatformMessagesSpy spy = PlatformMessagesSpy(); setUp(() async { - await window.debugSwitchBrowserHistory(useSingle: false); spy.setUp(); }); tearDown(() async { spy.tearDown(); - await setStrategy(null); + await window.debugResetHistory(); }); test('basic setup works', () async { - await setStrategy(TestLocationStrategy.fromEntry( - TestHistoryEntry('initial state', null, '/initial'))); + final TestUrlStrategy strategy = TestUrlStrategy.fromEntry( + TestHistoryEntry('initial state', null, '/initial'), + ); + await window.debugInitializeHistory(strategy, useSingle: false); // There should be only one entry. expect(strategy.history, hasLength(1)); @@ -273,7 +283,11 @@ void testMain() { skip: browserEngine == BrowserEngine.edge); test('browser back button push route infromation correctly', () async { - await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/home'))); + final TestUrlStrategy strategy = TestUrlStrategy.fromEntry( + TestHistoryEntry('initial state', null, '/home'), + ); + await window.debugInitializeHistory(strategy, useSingle: false); + // Initially, we should be on the flutter entry. expect(strategy.history, hasLength(1)); expect(strategy.currentEntry.state, _tagStateWithSerialCount('initial state', 0)); @@ -289,7 +303,7 @@ void testMain() { // No platform messages have been sent so far. expect(spy.messages, isEmpty); // Clicking back should take us to page1. - await strategy.back(); + await strategy.go(-1); // First, the framework should've received a `pushRouteInformation` // platform message. expect(spy.messages, hasLength(1)); @@ -310,7 +324,10 @@ void testMain() { skip: browserEngine == BrowserEngine.edge); test('multiple browser back clicks', () async { - await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/home'))); + final TestUrlStrategy strategy = TestUrlStrategy.fromEntry( + TestHistoryEntry('initial state', null, '/home'), + ); + await window.debugInitializeHistory(strategy, useSingle: false); await routeInfomrationUpdated('/page1', 'page1 state'); await routeInfomrationUpdated('/page2', 'page2 state'); @@ -322,7 +339,7 @@ void testMain() { expect(strategy.currentEntry.url, '/page2'); // Back to page1. - await strategy.back(); + await strategy.go(-1); // 1. The engine sends a `pushRouteInformation` platform message. expect(spy.messages, hasLength(1)); expect(spy.messages[0].channel, 'flutter/navigation'); @@ -338,7 +355,7 @@ void testMain() { expect(strategy.currentEntry.state, _tagStateWithSerialCount('page1 state', 1)); expect(strategy.currentEntry.url, '/page1'); // Back to home. - await strategy.back(); + await strategy.go(-1); // 1. The engine sends a `pushRouteInformation` platform message. expect(spy.messages, hasLength(1)); expect(spy.messages[0].channel, 'flutter/navigation'); @@ -359,7 +376,10 @@ void testMain() { browserEngine == BrowserEngine.webkit); test('handle user-provided url', () async { - await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/home'))); + final TestUrlStrategy strategy = TestUrlStrategy.fromEntry( + TestHistoryEntry('initial state', null, '/home'), + ); + await window.debugInitializeHistory(strategy, useSingle: false); await strategy.simulateUserTypingUrl('/page3'); // This delay is necessary to wait for [BrowserHistory] because it @@ -381,7 +401,7 @@ void testMain() { expect(strategy.currentEntry.url, '/page3'); // Back to home. - await strategy.back(); + await strategy.go(-1); // 1. The engine sends a `pushRouteInformation` platform message. expect(spy.messages, hasLength(1)); expect(spy.messages[0].channel, 'flutter/navigation'); @@ -401,7 +421,10 @@ void testMain() { skip: browserEngine == BrowserEngine.edge); test('forward button works', () async { - await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/home'))); + final TestUrlStrategy strategy = TestUrlStrategy.fromEntry( + TestHistoryEntry('initial state', null, '/home'), + ); + await window.debugInitializeHistory(strategy, useSingle: false); await routeInfomrationUpdated('/page1', 'page1 state'); await routeInfomrationUpdated('/page2', 'page2 state'); @@ -413,7 +436,7 @@ void testMain() { expect(strategy.currentEntry.url, '/page2'); // Back to page1. - await strategy.back(); + await strategy.go(-1); // 1. The engine sends a `pushRouteInformation` platform message. expect(spy.messages, hasLength(1)); expect(spy.messages[0].channel, 'flutter/navigation'); @@ -430,7 +453,7 @@ void testMain() { expect(strategy.currentEntry.url, '/page1'); // Forward to page2 - await strategy.back(count: -1); + await strategy.go(1); // 1. The engine sends a `pushRouteInformation` platform message. expect(spy.messages, hasLength(1)); expect(spy.messages[0].channel, 'flutter/navigation'); @@ -450,7 +473,7 @@ void testMain() { skip: browserEngine == BrowserEngine.edge); }); - group('$HashLocationStrategy', () { + group('$HashUrlStrategy', () { TestPlatformLocation location; setUp(() { @@ -462,26 +485,26 @@ void testMain() { }); test('leading slash is optional', () { - final HashLocationStrategy strategy = HashLocationStrategy(location); + final HashUrlStrategy strategy = HashUrlStrategy(location); location.hash = '#/'; - expect(strategy.path, '/'); + expect(strategy.getPath(), '/'); location.hash = '#/foo'; - expect(strategy.path, '/foo'); + expect(strategy.getPath(), '/foo'); location.hash = '#foo'; - expect(strategy.path, 'foo'); + expect(strategy.getPath(), 'foo'); }); test('path should not be empty', () { - final HashLocationStrategy strategy = HashLocationStrategy(location); + final HashUrlStrategy strategy = HashUrlStrategy(location); location.hash = ''; - expect(strategy.path, '/'); + expect(strategy.getPath(), '/'); location.hash = '#'; - expect(strategy.path, '/'); + expect(strategy.getPath(), '/'); }); }); } @@ -529,31 +552,31 @@ class TestPlatformLocation extends PlatformLocation { String hash; dynamic state; - void onPopState(html.EventListener fn) { + @override + void addPopStateListener(html.EventListener fn) { throw UnimplementedError(); } - void offPopState(html.EventListener fn) { - throw UnimplementedError(); - } - - void onHashChange(html.EventListener fn) { - throw UnimplementedError(); - } - - void offHashChange(html.EventListener fn) { + @override + void removePopStateListener(html.EventListener fn) { throw UnimplementedError(); } + @override void pushState(dynamic state, String title, String url) { throw UnimplementedError(); } + @override void replaceState(dynamic state, String title, String url) { throw UnimplementedError(); } - void back(int count) { + @override + void go(int count) { throw UnimplementedError(); } + + @override + String getBaseHref() => '/'; } diff --git a/lib/web_ui/test/engine/navigation_test.dart b/lib/web_ui/test/engine/navigation_test.dart index 44d3bf2939e95..99bae818f54a7 100644 --- a/lib/web_ui/test/engine/navigation_test.dart +++ b/lib/web_ui/test/engine/navigation_test.dart @@ -10,7 +10,7 @@ import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart' as engine; -engine.TestLocationStrategy _strategy; +engine.TestUrlStrategy _strategy; const engine.MethodCodec codec = engine.JSONMethodCodec(); @@ -21,12 +21,14 @@ void main() { } void testMain() { - setUp(() { - engine.window.locationStrategy = _strategy = engine.TestLocationStrategy(); + setUp(() async { + _strategy = engine.TestUrlStrategy(); + await engine.window.debugInitializeHistory(_strategy, useSingle: true); }); - tearDown(() { - engine.window.locationStrategy = _strategy = null; + tearDown(() async { + _strategy = null; + await engine.window.debugResetHistory(); }); test('Tracks pushed, replaced and popped routes', () async { @@ -40,6 +42,6 @@ void testMain() { (_) => completer.complete(), ); await completer.future; - expect(_strategy.path, '/foo'); + expect(_strategy.getPath(), '/foo'); }); } diff --git a/lib/web_ui/test/engine/surface/scene_builder_test.dart b/lib/web_ui/test/engine/surface/scene_builder_test.dart index da2bdb275215e..89b358e35f7f6 100644 --- a/lib/web_ui/test/engine/surface/scene_builder_test.dart +++ b/lib/web_ui/test/engine/surface/scene_builder_test.dart @@ -133,7 +133,8 @@ void testMain() { () { final PersistedScene scene1 = PersistedScene(null); final PersistedClipRect clip1 = - PersistedClipRect(null, const Rect.fromLTRB(10, 10, 20, 20)); + PersistedClipRect(null, const Rect.fromLTRB(10, 10, 20, 20), + Clip.antiAlias); final PersistedOpacity opacity = PersistedOpacity(null, 100, Offset.zero); final MockPersistedPicture picture = MockPersistedPicture(); @@ -158,7 +159,8 @@ void testMain() { // because the clip didn't change no repaints should happen. final PersistedScene scene2 = PersistedScene(scene1); final PersistedClipRect clip2 = - PersistedClipRect(clip1, const Rect.fromLTRB(10, 10, 20, 20)); + PersistedClipRect(clip1, const Rect.fromLTRB(10, 10, 20, 20), + Clip.antiAlias); clip1.state = PersistedSurfaceState.pendingUpdate; scene2.appendChild(clip2); opacity.state = PersistedSurfaceState.pendingRetention; @@ -176,7 +178,8 @@ void testMain() { // This should cause the picture to repaint despite being retained. final PersistedScene scene3 = PersistedScene(scene2); final PersistedClipRect clip3 = - PersistedClipRect(clip2, const Rect.fromLTRB(10, 10, 50, 50)); + PersistedClipRect(clip2, const Rect.fromLTRB(10, 10, 50, 50), + Clip.antiAlias); clip2.state = PersistedSurfaceState.pendingUpdate; scene3.appendChild(clip3); opacity.state = PersistedSurfaceState.pendingRetention; @@ -234,6 +237,7 @@ void testMain() { builder.pop(); html.HtmlElement content = builder.build().webOnlyRootElement; + html.document.body.append(content); expect(content.querySelector('canvas').style.zIndex, '-1'); // Force update to scene which will utilize reuse code path. @@ -627,8 +631,16 @@ Picture _drawPicture() { final EnginePictureRecorder recorder = PictureRecorder(); final RecordingCanvas canvas = recorder.beginRecording(const Rect.fromLTRB(0, 0, 400, 400)); + Shader gradient = Gradient.radial( + Offset(100, 100), 50, [ + const Color.fromARGB(255, 0, 0, 0), + const Color.fromARGB(255, 0, 0, 255) + ]); canvas.drawCircle( - Offset(offsetX + 10, offsetY + 10), 10, Paint()..style = PaintingStyle.fill); + Offset(offsetX + 10, offsetY + 10), 10, + Paint() + ..style = PaintingStyle.fill + ..shader = gradient); canvas.drawCircle( Offset(offsetX + 60, offsetY + 10), 10, @@ -656,8 +668,16 @@ Picture _drawPathImagePath() { final EnginePictureRecorder recorder = PictureRecorder(); final RecordingCanvas canvas = recorder.beginRecording(const Rect.fromLTRB(0, 0, 400, 400)); + Shader gradient = Gradient.radial( + Offset(100, 100), 50, [ + const Color.fromARGB(255, 0, 0, 0), + const Color.fromARGB(255, 0, 0, 255) + ]); canvas.drawCircle( - Offset(offsetX + 10, offsetY + 10), 10, Paint()..style = PaintingStyle.fill); + Offset(offsetX + 10, offsetY + 10), 10, + Paint() + ..style = PaintingStyle.fill + ..shader = gradient); canvas.drawCircle( Offset(offsetX + 60, offsetY + 10), 10, @@ -671,6 +691,11 @@ Picture _drawPathImagePath() { ..style = PaintingStyle.fill ..color = const Color.fromRGBO(0, 255, 0, 1)); canvas.drawImage(createTestImage(), Offset(0, 0), Paint()); + canvas.drawCircle( + Offset(offsetX + 10, offsetY + 10), 10, + Paint() + ..style = PaintingStyle.fill + ..shader = gradient); canvas.drawCircle( Offset(offsetX + 60, offsetY + 60), 10, diff --git a/lib/web_ui/test/engine/surface/shaders/normalized_gradient_test.dart b/lib/web_ui/test/engine/surface/shaders/normalized_gradient_test.dart new file mode 100644 index 0000000000000..b413bb65a220f --- /dev/null +++ b/lib/web_ui/test/engine/surface/shaders/normalized_gradient_test.dart @@ -0,0 +1,125 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui hide window; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + group('Shader Normalized Gradient', () { + test('3 stop at start', () { + NormalizedGradient gradient = NormalizedGradient([ + ui.Color(0xFF000000), ui.Color(0xFFFF7f3f) + ], stops: [0.0, 0.5]); + int res = _computeColorAt(gradient, 0.0); + assert(res == 0xFF000000); + res = _computeColorAt(gradient, 0.25); + assert(res == 0xFF7f3f1f); + res = _computeColorAt(gradient, 0.5); + assert(res == 0xFFFF7f3f); + res = _computeColorAt(gradient, 0.7); + assert(res == 0xFFFF7f3f); + res = _computeColorAt(gradient, 1.0); + assert(res == 0xFFFF7f3f); + }); + + test('3 stop at end', () { + NormalizedGradient gradient = NormalizedGradient([ + ui.Color(0xFF000000), ui.Color(0xFFFF7f3f) + ], stops: [0.5, 1.0]); + int res = _computeColorAt(gradient, 0.0); + assert(res == 0xFF000000); + res = _computeColorAt(gradient, 0.25); + assert(res == 0xFF000000); + res = _computeColorAt(gradient, 0.5); + assert(res == 0xFF000000); + res = _computeColorAt(gradient, 0.75); + assert(res == 0xFF7f3f1f); + res = _computeColorAt(gradient, 1.0); + assert(res == 0xFFFF7f3f); + }); + + test('4 stop', () { + NormalizedGradient gradient = NormalizedGradient([ + ui.Color(0xFF000000), ui.Color(0xFFFF7f3f) + ], stops: [0.25, 0.5]); + int res = _computeColorAt(gradient, 0.0); + assert(res == 0xFF000000); + res = _computeColorAt(gradient, 0.25); + assert(res == 0xFF000000); + res = _computeColorAt(gradient, 0.4); + assert(res == 0xFF994c25); + res = _computeColorAt(gradient, 0.5); + assert(res == 0xFFFF7f3f); + res = _computeColorAt(gradient, 0.75); + assert(res == 0xFFFF7f3f); + res = _computeColorAt(gradient, 1.0); + assert(res == 0xFFFF7f3f); + }); + + test('5 stop', () { + NormalizedGradient gradient = NormalizedGradient([ + ui.Color(0x10000000), ui.Color(0x20FF0000), + ui.Color(0x4000FF00), ui.Color(0x800000FF), + ui.Color(0xFFFFFFFF) + ], stops: [0.0, 0.1, 0.2, 0.5, 1.0]); + int res = _computeColorAt(gradient, 0.0); + assert(res == 0x10000000); + res = _computeColorAt(gradient, 0.05); + assert(res == 0x187f0000); + res = _computeColorAt(gradient, 0.1); + assert(res == 0x20ff0000); + res = _computeColorAt(gradient, 0.15); + assert(res == 0x307f7f00); + res = _computeColorAt(gradient, 0.2); + assert(res == 0x4000ff00); + res = _computeColorAt(gradient, 0.4); + assert(res == 0x6a0054a9); + res = _computeColorAt(gradient, 0.5); + assert(res == 0x800000fe); + res = _computeColorAt(gradient, 0.9); + assert(res == 0xe5ccccff); + res = _computeColorAt(gradient, 1.0); + assert(res == 0xffffffff); + }); + + test('2 stops at ends', () { + NormalizedGradient gradient = NormalizedGradient([ + ui.Color(0x00000000), ui.Color(0xFFFFFFFF) + ]); + int res = _computeColorAt(gradient, 0.0); + assert(res == 0); + res = _computeColorAt(gradient, 1.0); + assert(res == 0xFFFFFFFF); + res = _computeColorAt(gradient, 0.5); + assert(res == 0x7f7f7f7f); + }); + }); +} + +int _computeColorAt(NormalizedGradient gradient, double t) { + int i = 0; + while (t > gradient.thresholdAt(i + 1)) { + ++i; + } + double r = t * gradient.scaleAt(i * 4) + gradient.biasAt(i * 4); + double g = t * gradient.scaleAt(i * 4 + 1) + gradient.biasAt(i * 4 + 1); + double b = t * gradient.scaleAt(i * 4 + 2) + gradient.biasAt(i * 4 + 2); + double a = t * gradient.scaleAt(i * 4 + 3) + gradient.biasAt(i * 4 + 3); + int val = 0; + val |= (a * 0xFF).toInt() & 0xFF; + val<<=8; + val |= (r * 0xFF).toInt() & 0xFF; + val<<=8; + val |= (g * 0xFF).toInt() & 0xFF; + val<<=8; + val |= (b * 0xFF).toInt() & 0xFF; + return val; +} diff --git a/lib/web_ui/test/engine/surface/shaders/shader_builder_test.dart b/lib/web_ui/test/engine/surface/shaders/shader_builder_test.dart new file mode 100644 index 0000000000000..336cf4f8d8890 --- /dev/null +++ b/lib/web_ui/test/engine/surface/shaders/shader_builder_test.dart @@ -0,0 +1,208 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' hide window; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + String mat2Sample = 'mat2(1.1, 2.1, 1.2, 2.2)'; + String mat3Sample = 'mat3(1.1, 2.1, 3.1, // first column (not row!)\n' + '1.2, 2.2, 3.2, // second column\n' + '1.3, 2.3, 3.3 // third column\n' + ')'; + String mat4Sample = 'mat3(1.1, 2.1, 3.1, 4.1,\n' + '1.2, 2.2, 3.2, 4.2,\n' + '1.3, 2.3, 3.3, 4.3,\n' + '1.4, 2.4, 3.4, 4.4,\n' + ')'; + + setUpAll(() async { + await webOnlyInitializeEngine(); + }); + + group('Shader Declarations', () { + test('Constant declaration WebGL1', () { + ShaderBuilder builder = ShaderBuilder(WebGLVersion.webgl1); + builder.addConst(ShaderType.kBool, 'false'); + builder.addConst(ShaderType.kInt, '0'); + builder.addConst(ShaderType.kFloat, '1.0'); + builder.addConst(ShaderType.kBVec2, 'bvec2(false, false)'); + builder.addConst(ShaderType.kBVec3, 'bvec3(false, false, true)'); + builder.addConst(ShaderType.kBVec4, 'bvec4(true, true, false, false)'); + builder.addConst(ShaderType.kIVec2, 'ivec2(1, 2)'); + builder.addConst(ShaderType.kIVec3, 'ivec3(1, 2, 3)'); + builder.addConst(ShaderType.kIVec4, 'ivec4(1, 2, 3, 4)'); + builder.addConst(ShaderType.kVec2, 'vec2(1.0, 2.0)'); + builder.addConst(ShaderType.kVec3, 'vec3(1.0, 2.0, 3.0)'); + builder.addConst(ShaderType.kVec4, 'vec4(1.0, 2.0, 3.0, 4.0)'); + builder.addConst(ShaderType.kMat2, mat2Sample); + builder.addConst(ShaderType.kMat2, mat2Sample, name: 'transform1'); + builder.addConst(ShaderType.kMat3, mat3Sample); + builder.addConst(ShaderType.kMat4, mat4Sample); + expect( + builder.build(), + 'const bool c_0 = false;\n' + 'const int c_1 = 0;\n' + 'const float c_2 = 1.0;\n' + 'const bvec2 c_3 = bvec2(false, false);\n' + 'const bvec3 c_4 = bvec3(false, false, true);\n' + 'const bvec4 c_5 = bvec4(true, true, false, false);\n' + 'const ivec2 c_6 = ivec2(1, 2);\n' + 'const ivec3 c_7 = ivec3(1, 2, 3);\n' + 'const ivec4 c_8 = ivec4(1, 2, 3, 4);\n' + 'const vec2 c_9 = vec2(1.0, 2.0);\n' + 'const vec3 c_10 = vec3(1.0, 2.0, 3.0);\n' + 'const vec4 c_11 = vec4(1.0, 2.0, 3.0, 4.0);\n' + 'const mat2 c_12 = $mat2Sample;\n' + 'const mat2 transform1 = ${mat2Sample};\n' + 'const mat3 c_13 = ${mat3Sample};\n' + 'const mat4 c_14 = ${mat4Sample};\n'); + }); + + test('Constant declaration WebGL2', () { + ShaderBuilder builder = ShaderBuilder(WebGLVersion.webgl2); + builder.addConst(ShaderType.kBool, 'false'); + builder.addConst(ShaderType.kInt, '0'); + builder.addConst(ShaderType.kFloat, '1.0'); + builder.addConst(ShaderType.kBVec2, 'bvec2(false, false)'); + builder.addConst(ShaderType.kBVec3, 'bvec3(false, false, true)'); + builder.addConst(ShaderType.kBVec4, 'bvec4(true, true, false, false)'); + builder.addConst(ShaderType.kIVec2, 'ivec2(1, 2)'); + builder.addConst(ShaderType.kIVec3, 'ivec3(1, 2, 3)'); + builder.addConst(ShaderType.kIVec4, 'ivec4(1, 2, 3, 4)'); + builder.addConst(ShaderType.kVec2, 'vec2(1.0, 2.0)'); + builder.addConst(ShaderType.kVec3, 'vec3(1.0, 2.0, 3.0)'); + builder.addConst(ShaderType.kVec4, 'vec4(1.0, 2.0, 3.0, 4.0)'); + builder.addConst(ShaderType.kMat2, mat2Sample); + builder.addConst(ShaderType.kMat2, mat2Sample, name: 'transform2'); + builder.addConst(ShaderType.kMat3, mat3Sample); + builder.addConst(ShaderType.kMat4, mat4Sample); + expect( + builder.build(), + '#version 300 es\n' + 'const bool c_0 = false;\n' + 'const int c_1 = 0;\n' + 'const float c_2 = 1.0;\n' + 'const bvec2 c_3 = bvec2(false, false);\n' + 'const bvec3 c_4 = bvec3(false, false, true);\n' + 'const bvec4 c_5 = bvec4(true, true, false, false);\n' + 'const ivec2 c_6 = ivec2(1, 2);\n' + 'const ivec3 c_7 = ivec3(1, 2, 3);\n' + 'const ivec4 c_8 = ivec4(1, 2, 3, 4);\n' + 'const vec2 c_9 = vec2(1.0, 2.0);\n' + 'const vec3 c_10 = vec3(1.0, 2.0, 3.0);\n' + 'const vec4 c_11 = vec4(1.0, 2.0, 3.0, 4.0);\n' + 'const mat2 c_12 = $mat2Sample;\n' + 'const mat2 transform2 = ${mat2Sample};\n' + 'const mat3 c_13 = ${mat3Sample};\n' + 'const mat4 c_14 = ${mat4Sample};\n'); + }); + + test('Attribute declaration WebGL1', () { + ShaderBuilder builder = ShaderBuilder(WebGLVersion.webgl1); + builder.addIn(ShaderType.kVec4, name: 'position'); + builder.addIn(ShaderType.kVec4); + expect( + builder.build(), + 'attribute vec4 position;\n' + 'attribute vec4 attr_0;\n'); + }); + + test('in declaration WebGL1', () { + ShaderBuilder builder = ShaderBuilder.fragment(WebGLVersion.webgl1); + builder.addIn(ShaderType.kVec4, name: 'position'); + builder.addIn(ShaderType.kVec4); + expect( + builder.build(), + 'varying vec4 position;\n' + 'varying vec4 attr_0;\n'); + }); + + test('Attribute declaration WebGL2', () { + ShaderBuilder builder = ShaderBuilder(WebGLVersion.webgl2); + builder.addIn(ShaderType.kVec4, name: 'position'); + builder.addIn(ShaderType.kVec4); + expect( + builder.build(), + '#version 300 es\n' + 'in vec4 position;\n' + 'in vec4 attr_0;\n'); + }); + + test('Uniform declaration WebGL1', () { + ShaderBuilder builder = ShaderBuilder(WebGLVersion.webgl1); + ShaderDeclaration variable = + builder.addUniform(ShaderType.kVec4, name: 'v1'); + expect(variable.name, 'v1'); + expect(variable.dataType, ShaderType.kVec4); + expect(variable.storage, ShaderStorageQualifier.kUniform); + builder.addUniform(ShaderType.kVec4); + expect( + builder.build(), + 'uniform vec4 v1;\n' + 'uniform vec4 uni_0;\n'); + }); + + test('Uniform declaration WebGL2', () { + ShaderBuilder builder = ShaderBuilder(WebGLVersion.webgl2); + ShaderDeclaration variable = + builder.addUniform(ShaderType.kVec4, name: 'v1'); + expect(variable.name, 'v1'); + expect(variable.dataType, ShaderType.kVec4); + expect(variable.storage, ShaderStorageQualifier.kUniform); + builder.addUniform(ShaderType.kVec4); + expect( + builder.build(), + '#version 300 es\n' + 'uniform vec4 v1;\n' + 'uniform vec4 uni_0;\n'); + }); + + test('Float precision', () { + ShaderBuilder builder = ShaderBuilder(WebGLVersion.webgl2); + builder.floatPrecision = ShaderPrecision.kLow; + builder.addUniform(ShaderType.kFloat, name: 'f1'); + expect( + builder.build(), + '#version 300 es\n' + 'precision lowp float;\n' + 'uniform float f1;\n'); + }); + + test('Integer precision', () { + ShaderBuilder builder = ShaderBuilder(WebGLVersion.webgl2); + builder.integerPrecision = ShaderPrecision.kLow; + builder.addUniform(ShaderType.kInt, name: 'i1'); + expect( + builder.build(), + '#version 300 es\n' + 'precision lowp int;\n' + 'uniform int i1;\n'); + }); + + test('Method', () { + ShaderBuilder builder = ShaderBuilder(WebGLVersion.webgl2); + builder.floatPrecision = ShaderPrecision.kMedium; + ShaderDeclaration variable = + builder.addUniform(ShaderType.kFloat, name: 'f1'); + ShaderMethod m = builder.addMethod('main'); + m.addStatement('f1 = 5.0;'); + expect( + builder.build(), + '#version 300 es\n' + 'precision mediump float;\n' + 'uniform float ${variable.name};\n' + 'void main() {\n' + ' f1 = 5.0;\n' + '}\n'); + }); + }); +} diff --git a/lib/web_ui/test/golden_tests/engine/canvas_blend_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_blend_golden_test.dart index 39cfcb4b27feb..2996be4e1be01 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_blend_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_blend_golden_test.dart @@ -24,7 +24,7 @@ void testMain() async { // Commit a recording canvas to a bitmap, and compare with the expected Future _checkScreenshot(RecordingCanvas rc, String fileName, {Rect region = const Rect.fromLTWH(0, 0, 500, 500), - double maxDiffRatePercent = 0.0}) async { + double maxDiffRatePercent = 0.0, bool write = false}) async { final EngineCanvas engineCanvas = BitmapCanvas(screenRect); rc.endRecording(); @@ -35,7 +35,8 @@ void testMain() async { try { sceneElement.append(engineCanvas.rootElement); html.document.body.append(sceneElement); - await matchGoldenFile('$fileName.png', region: region, maxDiffRatePercent: maxDiffRatePercent); + await matchGoldenFile('$fileName.png', region: region, + maxDiffRatePercent: maxDiffRatePercent, write: write); } finally { // The page is reused across tests, so remove the element after taking the // Scuba screenshot. @@ -83,7 +84,8 @@ void testMain() async { ..color = const Color.fromARGB(128, 255, 0, 0)); rc.restore(); await _checkScreenshot(rc, 'canvas_blend_circle_diff_color', - maxDiffRatePercent: operatingSystem == OperatingSystem.macOs ? 2.95 : 0); + maxDiffRatePercent: operatingSystem == OperatingSystem.macOs ? 2.95 : + operatingSystem == OperatingSystem.iOs ? 1.0 : 0); }); test('Blend circle and text with multiply', () async { @@ -120,7 +122,8 @@ void testMain() async { Paint()..blendMode = BlendMode.multiply); rc.restore(); await _checkScreenshot(rc, 'canvas_blend_image_multiply', - maxDiffRatePercent: operatingSystem == OperatingSystem.macOs ? 2.95 : 0); + maxDiffRatePercent: operatingSystem == OperatingSystem.macOs ? 2.95 : + operatingSystem == OperatingSystem.iOs ? 2.0 : 0); }); } diff --git a/lib/web_ui/test/golden_tests/engine/canvas_draw_color_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_draw_color_test.dart new file mode 100644 index 0000000000000..112bffb5d58f7 --- /dev/null +++ b/lib/web_ui/test/golden_tests/engine/canvas_draw_color_test.dart @@ -0,0 +1,90 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.6 +import 'dart:html' as html; + +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/ui.dart'; +import 'package:ui/src/engine.dart'; + +import 'package:web_engine_tester/golden_tester.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { + setUp(() async { + debugShowClipLayers = true; + SurfaceSceneBuilder.debugForgetFrameScene(); + for (html.Node scene in html.document.querySelectorAll('flt-scene')) { + scene.remove(); + } + + await webOnlyInitializePlatform(); + webOnlyFontCollection.debugRegisterTestFonts(); + await webOnlyFontCollection.ensureFontsLoaded(); + }); + + test('drawColor should cover entire viewport', () async { + final Rect region = Rect.fromLTWH(0, 0, 400, 400); + + final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); + final Picture testPicture = _drawTestPicture(region, useColor: true); + builder.addPicture(Offset.zero, testPicture); + + html.document.body.append(builder + .build() + .webOnlyRootElement); + + await matchGoldenFile('canvas_draw_color.png', region: region); + }, skip: true); // TODO: matchGolden fails when a div covers viewport. + + test('drawPaint should cover entire viewport', () async { + final Rect region = Rect.fromLTWH(0, 0, 400, 400); + + final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); + final Picture testPicture = _drawTestPicture(region, useColor: false); + builder.addPicture(Offset.zero, testPicture); + + html.document.body.append(builder + .build() + .webOnlyRootElement); + + await matchGoldenFile('canvas_draw_paint.png', region: region); + }, skip: true); // TODO: matchGolden fails when a div covers viewport.); +} + +Picture _drawTestPicture(Rect region, {bool useColor = false}) { + final EnginePictureRecorder recorder = PictureRecorder(); + final Rect r = Rect.fromLTWH(0, 0, 200, 200); + final RecordingCanvas canvas = recorder.beginRecording(r); + + canvas.drawRect( + region.deflate(8.0), + Paint() + ..style = PaintingStyle.fill + ..color = Color(0xFFE0E0E0) + ); + + canvas.transform(Matrix4.translationValues(50, 50, 0).storage); + + if (useColor) { + canvas.drawColor(const Color.fromRGBO(0, 255, 0, 1), BlendMode.srcOver); + } else { + canvas.drawPaint(Paint() + ..style = PaintingStyle.fill + ..color = const Color.fromRGBO(0, 0, 255, 1)); + } + + canvas.drawCircle( + Offset(r.width/2, r.height/2), r.width/2, + Paint() + ..style = PaintingStyle.fill + ..color = const Color.fromRGBO(255, 0, 0, 1)); + + return recorder.endRecording(); +} diff --git a/lib/web_ui/test/golden_tests/engine/canvas_draw_image_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_draw_image_golden_test.dart index 799ab41050a2e..4a61124d2578a 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_draw_image_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_draw_image_golden_test.dart @@ -28,7 +28,8 @@ void testMain() async { // Commit a recording canvas to a bitmap, and compare with the expected Future _checkScreenshot(RecordingCanvas rc, String fileName, {Rect region = const Rect.fromLTWH(0, 0, 500, 500), - double maxDiffRatePercent = 0.0}) async { + double maxDiffRatePercent = 0.0, bool setupPerspective = false, + bool write = false}) async { final EngineCanvas engineCanvas = BitmapCanvas(screenRect); rc.endRecording(); @@ -37,10 +38,18 @@ void testMain() async { // Wrap in so that our CSS selectors kick in. final html.Element sceneElement = html.Element.tag('flt-scene'); try { + if (setupPerspective) { + // iFrame disables perspective, set it explicitly for test. + engineCanvas.rootElement.style.perspective = '400px'; + for (html.Element element in engineCanvas.rootElement.querySelectorAll( + 'div')) { + element.style.perspective = '400px'; + } + } sceneElement.append(engineCanvas.rootElement); html.document.body.append(sceneElement); await matchGoldenFile('$fileName.png', - region: region, maxDiffRatePercent: maxDiffRatePercent); + region: region, maxDiffRatePercent: maxDiffRatePercent, write: write); } finally { // The page is reused across tests, so remove the element after taking the // Scuba screenshot. @@ -400,6 +409,158 @@ void testMain() async { await _checkScreenshot(canvas, 'draw_clipped_and_transformed_image', region: region, maxDiffRatePercent: 1.0); }); + + /// Regression test for https://github.com/flutter/flutter/issues/61245 + test('Should render image with perspective', () async { + final Rect region = const Rect.fromLTRB(0, 0, 200, 200); + final RecordingCanvas canvas = RecordingCanvas(region); + canvas.translate(10, 10); + canvas.drawImage(createTestImage(), Offset(0, 0), new Paint()); + Matrix4 transform = Matrix4.identity() + ..setRotationY(0.8) + ..setEntry(3, 2, 0.0005); // perspective + canvas.transform(transform.storage); + canvas.drawImage(createTestImage(), Offset(0, 100), new Paint()); + await _checkScreenshot(canvas, 'draw_3d_image', + region: region, + maxDiffRatePercent: 6.0, + setupPerspective: true); + }); + + /// Regression test for https://github.com/flutter/flutter/issues/61245 + test('Should render image with perspective inside clip area', () async { + final Rect region = const Rect.fromLTRB(0, 0, 200, 200); + final RecordingCanvas canvas = RecordingCanvas(region); + canvas.drawRect(region, Paint()..color = Color(0xFFE0E0E0)); + canvas.translate(10, 10); + canvas.drawImage(createTestImage(), Offset(0, 0), new Paint()); + Matrix4 transform = Matrix4.identity() + ..setRotationY(0.8) + ..setEntry(3, 2, 0.0005); // perspective + canvas.transform(transform.storage); + canvas.clipRect(region, ClipOp.intersect); + canvas.drawRect(Rect.fromLTWH(0, 0, 100, 200), Paint()..color = Color(0x801080E0)); + canvas.drawImage(createTestImage(), Offset(0, 100), new Paint()); + canvas.drawRect(Rect.fromLTWH(50, 150, 50, 20), Paint()..color = Color(0x80000000)); + await _checkScreenshot(canvas, 'draw_3d_image_clipped', + region: region, + maxDiffRatePercent: 5.0, + setupPerspective: true); + }); + + test('Should render rect with perspective transform', () async { + final Rect region = const Rect.fromLTRB(0, 0, 400, 400); + final RecordingCanvas canvas = RecordingCanvas(region); + canvas.drawRect(region, Paint()..color = Color(0xFFE0E0E0)); + canvas.translate(20, 20); + canvas.drawRect(Rect.fromLTWH(0, 0, 100, 40), + Paint()..color = Color(0xFF000000)); + Matrix4 transform = Matrix4.identity() + ..setRotationY(0.8) + ..setEntry(3, 2, 0.001); // perspective + canvas.transform(transform.storage); + canvas.clipRect(region, ClipOp.intersect); + canvas.drawRect(Rect.fromLTWH(0, 60, 120, 40), Paint()..color = Color(0x801080E0)); + canvas.drawRect(Rect.fromLTWH(300, 250, 120, 40), Paint()..color = Color(0x80E010E0)); + canvas.drawRRect(RRect.fromRectAndRadius(Rect.fromLTWH(0, 120, 160, 40), Radius.circular(5)), + Paint()..color = Color(0x801080E0)); + canvas.drawRRect(RRect.fromRectAndRadius(Rect.fromLTWH(300, 320, 90, 40), Radius.circular(20)), + Paint()..color = Color(0x80E010E0)); + await _checkScreenshot(canvas, 'draw_3d_rect_clipped', + region: region, + maxDiffRatePercent: 1.0, + setupPerspective: true); + }); + + test('Should render color and ovals with perspective transform', () async { + final Rect region = const Rect.fromLTRB(0, 0, 400, 400); + final RecordingCanvas canvas = RecordingCanvas(region); + canvas.drawRect(region, Paint()..color = Color(0xFFFF0000)); + canvas.drawColor(Color(0xFFE0E0E0), BlendMode.src); + canvas.translate(20, 20); + canvas.drawRect(Rect.fromLTWH(0, 0, 100, 40), + Paint()..color = Color(0xFF000000)); + Matrix4 transform = Matrix4.identity() + ..setRotationY(0.8) + ..setEntry(3, 2, 0.001); // perspective + canvas.transform(transform.storage); + canvas.clipRect(region, ClipOp.intersect); + canvas.drawOval(Rect.fromLTWH(0, 120, 130, 40), + Paint()..color = Color(0x801080E0)); + canvas.drawOval(Rect.fromLTWH(300, 290, 90, 40), + Paint()..color = Color(0x80E010E0)); + canvas.drawCircle(Offset(60, 240), 50, Paint()..color = Color(0x801080E0)); + canvas.drawCircle(Offset(360, 370), 30, Paint()..color = Color(0x80E010E0)); + await _checkScreenshot(canvas, 'draw_3d_oval_clipped', + region: region, + maxDiffRatePercent: 1.0, + setupPerspective: true); + }); + + test('Should render path with perspective transform', () async { + final Rect region = const Rect.fromLTRB(0, 0, 400, 400); + final RecordingCanvas canvas = RecordingCanvas(region); + canvas.drawRect(region, Paint()..color = Color(0xFFFF0000)); + canvas.drawColor(Color(0xFFE0E0E0), BlendMode.src); + canvas.translate(20, 20); + canvas.drawRect(Rect.fromLTWH(0, 0, 100, 20), + Paint()..color = Color(0xFF000000)); + Matrix4 transform = Matrix4.identity() + ..setRotationY(0.8) + ..setEntry(3, 2, 0.001); // perspective + canvas.transform(transform.storage); + canvas.drawRect(Rect.fromLTWH(0, 120, 130, 40), + Paint()..color = Color(0x801080E0)); + canvas.drawOval(Rect.fromLTWH(300, 290, 90, 40), + Paint()..color = Color(0x80E010E0)); + Path path = Path(); + path.moveTo(50, 50); + path.lineTo(100, 50); + path.lineTo(100, 100); + path.close(); + canvas.drawPath(path, Paint()..color = Color(0x801080E0)); + + canvas.drawCircle(Offset(50, 50), 4, Paint()..color = Color(0xFF000000)); + canvas.drawCircle(Offset(100, 100), 4, Paint()..color = Color(0xFF000000)); + canvas.drawCircle(Offset(100, 50), 4, Paint()..color = Color(0xFF000000)); + await _checkScreenshot(canvas, 'draw_3d_path', + region: region, + maxDiffRatePercent: 1.0, + setupPerspective: true); + }); + + test('Should render path with perspective transform', () async { + final Rect region = const Rect.fromLTRB(0, 0, 400, 400); + final RecordingCanvas canvas = RecordingCanvas(region); + canvas.drawRect(region, Paint()..color = Color(0xFFFF0000)); + canvas.drawColor(Color(0xFFE0E0E0), BlendMode.src); + canvas.translate(20, 20); + canvas.drawRect(Rect.fromLTWH(0, 0, 100, 20), + Paint()..color = Color(0xFF000000)); + Matrix4 transform = Matrix4.identity() + ..setRotationY(0.8) + ..setEntry(3, 2, 0.001); // perspective + canvas.transform(transform.storage); + //canvas.clipRect(region, ClipOp.intersect); + canvas.drawRect(Rect.fromLTWH(0, 120, 130, 40), + Paint()..color = Color(0x801080E0)); + canvas.drawOval(Rect.fromLTWH(300, 290, 90, 40), + Paint()..color = Color(0x80E010E0)); + Path path = Path(); + path.moveTo(50, 50); + path.lineTo(100, 50); + path.lineTo(100, 100); + path.close(); + canvas.drawPath(path, Paint()..color = Color(0x801080E0)); + + canvas.drawCircle(Offset(50, 50), 4, Paint()..color = Color(0xFF000000)); + canvas.drawCircle(Offset(100, 100), 4, Paint()..color = Color(0xFF000000)); + canvas.drawCircle(Offset(100, 50), 4, Paint()..color = Color(0xFF000000)); + await _checkScreenshot(canvas, 'draw_3d_path_clipped', + region: region, + maxDiffRatePercent: 1.0, + setupPerspective: true); + }); } // 9 slice test image that has a shiny/glass look. diff --git a/lib/web_ui/test/golden_tests/engine/canvas_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_golden_test.dart index a072d025974fd..0590725f909fc 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_golden_test.dart @@ -93,28 +93,31 @@ void testMain() async { // compensate by shifting the contents of the canvas in the opposite // direction. canvas = BitmapCanvas(const Rect.fromLTWH(0.5, 0.5, 60, 60)); - + canvas.clipRect(const Rect.fromLTWH(0, 0, 50, 50), ClipOp.intersect); drawMisalignedLines(canvas); appendToScene(); - await matchGoldenFile('misaligned_canvas_test.png', region: region); + await matchGoldenFile('misaligned_canvas_test.png', region: region, + maxDiffRatePercent: 1.0); }); test('fill the whole canvas with color even when transformed', () async { canvas = BitmapCanvas(const Rect.fromLTWH(0, 0, 50, 50)); - + canvas.clipRect(const Rect.fromLTWH(0, 0, 50, 50), ClipOp.intersect); canvas.translate(25, 25); canvas.drawColor(const Color.fromRGBO(0, 255, 0, 1.0), BlendMode.src); appendToScene(); - await matchGoldenFile('bitmap_canvas_fills_color_when_transformed.png', region: region); + await matchGoldenFile('bitmap_canvas_fills_color_when_transformed.png', + region: region, + maxDiffRatePercent: 5.0); }); test('fill the whole canvas with paint even when transformed', () async { canvas = BitmapCanvas(const Rect.fromLTWH(0, 0, 50, 50)); - + canvas.clipRect(const Rect.fromLTWH(0, 0, 50, 50), ClipOp.intersect); canvas.translate(25, 25); canvas.drawPaint(SurfacePaintData() ..color = const Color.fromRGBO(0, 255, 0, 1.0) @@ -122,7 +125,9 @@ void testMain() async { appendToScene(); - await matchGoldenFile('bitmap_canvas_fills_paint_when_transformed.png', region: region); + await matchGoldenFile('bitmap_canvas_fills_paint_when_transformed.png', + region: region, + maxDiffRatePercent: 5.0); }); // This test reproduces text blurriness when two pieces of text appear inside @@ -157,9 +162,9 @@ void testMain() async { canvas = BitmapCanvas(canvasSize); canvas.debugChildOverdraw = true; - canvas.clipRect(outerClip); + canvas.clipRect(outerClip, ClipOp.intersect); canvas.drawParagraph(paragraph, const Offset(8.5, 8.5)); - canvas.clipRect(innerClip); + canvas.clipRect(innerClip, ClipOp.intersect); canvas.drawParagraph(paragraph, Offset(8.5, 8.5 + innerClip.top)); expect( @@ -245,7 +250,7 @@ void testMain() async { await matchGoldenFile( 'bitmap_canvas_draws_text_on_top_of_canvas.png', region: canvasSize, - maxDiffRatePercent: 0.0, + maxDiffRatePercent: 1.0, pixelComparison: PixelComparison.precise, ); }); diff --git a/lib/web_ui/test/golden_tests/engine/clip_op_golden_test.dart b/lib/web_ui/test/golden_tests/engine/clip_op_golden_test.dart new file mode 100644 index 0000000000000..8e14c37caa308 --- /dev/null +++ b/lib/web_ui/test/golden_tests/engine/clip_op_golden_test.dart @@ -0,0 +1,42 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/ui.dart'; +import 'package:ui/src/engine.dart'; +import 'screenshot.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { + setUp(() async { + debugEmulateFlutterTesterEnvironment = true; + }); + + /// Regression test for https://github.com/flutter/flutter/issues/64734. + test('Clips using difference', () async { + final Rect region = const Rect.fromLTRB(0, 0, 400, 300); + final RecordingCanvas canvas = RecordingCanvas(region); + final Rect titleRect = Rect.fromLTWH(20, 0, 50, 20); + final Paint paint = Paint() + ..style = PaintingStyle.stroke + ..color = const Color(0xff000000) + ..strokeWidth = 1; + canvas.save(); + try { + final Rect borderRect = Rect.fromLTRB(0, 10, region.width, region.height); + canvas.clipRect(titleRect, ClipOp.difference); + canvas.drawRect(borderRect, paint); + } finally { + canvas.restore(); + } + canvas..drawRect(titleRect, paint); + await canvasScreenshot(canvas, 'clip_op_difference', + region: const Rect.fromLTRB(0, 0, 420, 360)); + }); +} diff --git a/lib/web_ui/test/golden_tests/engine/compositing_golden_test.dart b/lib/web_ui/test/golden_tests/engine/compositing_golden_test.dart index 04dce71d0b986..8c2784c7ba03a 100644 --- a/lib/web_ui/test/golden_tests/engine/compositing_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/compositing_golden_test.dart @@ -22,7 +22,9 @@ void main() { void testMain() async { setUp(() async { - debugShowClipLayers = true; + // To debug test failures uncomment the following to visualize clipping + // layers: + // debugShowClipLayers = true; SurfaceSceneBuilder.debugForgetFrameScene(); for (html.Node scene in html.document.querySelectorAll('flt-scene')) { scene.remove(); @@ -545,7 +547,6 @@ void _testCullRectComputation() { 'renders clipped text with high quality', () async { // To reproduce blurriness we need real clipping. - debugShowClipLayers = false; final Paragraph paragraph = (ParagraphBuilder(ParagraphStyle(fontFamily: 'Roboto'))..addText('Am I blurry?')).build(); paragraph.layout(const ParagraphConstraints(width: 1000)); diff --git a/lib/web_ui/test/golden_tests/engine/gradient_golden_test.dart b/lib/web_ui/test/golden_tests/engine/gradient_golden_test.dart new file mode 100644 index 0000000000000..196938ff5eeed --- /dev/null +++ b/lib/web_ui/test/golden_tests/engine/gradient_golden_test.dart @@ -0,0 +1,318 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.6 +import 'dart:html' as html; +import 'dart:math' as math; +import 'dart:typed_data'; + +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/ui.dart'; +import 'package:ui/src/engine.dart'; + +import 'package:web_engine_tester/golden_tester.dart'; + +import 'scuba.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { + const double screenWidth = 600.0; + const double screenHeight = 800.0; + const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); + + // Commit a recording canvas to a bitmap, and compare with the expected + Future _checkScreenshot(RecordingCanvas rc, String fileName, + {Rect region = const Rect.fromLTWH(0, 0, 500, 240), + double maxDiffRatePercent = 0.0, bool write: false}) async { + final EngineCanvas engineCanvas = BitmapCanvas(screenRect); + + rc.endRecording(); + rc.apply(engineCanvas, screenRect); + + // Wrap in so that our CSS selectors kick in. + final html.Element sceneElement = html.Element.tag('flt-scene'); + try { + sceneElement.append(engineCanvas.rootElement); + html.document.body.append(sceneElement); + await matchGoldenFile('$fileName.png', + region: region, maxDiffRatePercent: maxDiffRatePercent, write: write); + } finally { + // The page is reused across tests, so remove the element after taking the + // Scuba screenshot. + sceneElement.remove(); + } + } + + setUp(() async { + debugEmulateFlutterTesterEnvironment = true; + }); + + setUpStableTestFonts(); + + test('Paints sweep gradient rectangles', () async { + final RecordingCanvas canvas = + RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); + canvas.save(); + + final Paint borderPaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 1 + ..color = Color(0xFF000000); + + List colors = [ + Color(0xFF000000), + Color(0xFFFF3C38), + Color(0xFFFF8C42), + Color(0xFFFFF275), + Color(0xFF6699CC), + Color(0xFF656D78),]; + List stops = [0.0, 0.05, 0.4, 0.6, 0.9, 1.0]; + + EngineGradient sweepGradient = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.clamp, + 0, 360.0 / 180.0 * math.pi, + Matrix4.rotationZ(math.pi / 6.0).storage); + + EngineGradient sweepGradientRotated = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.clamp, + 0, 360.0 / 180.0 * math.pi, + Matrix4.rotationZ(math.pi / 6.0).storage); + + const double kBoxWidth = 150; + const double kBoxHeight = 80; + // Gradient with default center. + Rect rectBounds = Rect.fromLTWH(10, 20, kBoxWidth, kBoxHeight); + canvas.drawRect(rectBounds, + Paint()..shader = engineGradientToShader(sweepGradient, rectBounds)); + canvas.drawRect(rectBounds, borderPaint); + + // Gradient with shifted center and rotation. + rectBounds = rectBounds.translate(kBoxWidth + 10, 0); + canvas.drawRect(rectBounds, + Paint()..shader = engineGradientToShader(sweepGradientRotated, Rect.fromLTWH(rectBounds.center.dx, rectBounds.top, rectBounds.width / 2, rectBounds.height))); + canvas.drawRect(rectBounds, borderPaint); + + // Gradient with start/endangle. + sweepGradient = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.clamp, + math.pi / 6, 3 * math.pi / 4, + Matrix4.rotationZ(math.pi / 6.0).storage); + + rectBounds = rectBounds.translate(kBoxWidth + 10, 0); + canvas.drawRect(rectBounds, + new Paint()..shader = engineGradientToShader(sweepGradient, rectBounds)); + canvas.drawRect(rectBounds, borderPaint); + + // Tile mode repeat + rectBounds = Rect.fromLTWH(10, 110, kBoxWidth, kBoxHeight); + sweepGradient = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.repeated, + math.pi / 6, 3 * math.pi / 4, + Matrix4.rotationZ(math.pi / 6.0).storage); + + canvas.drawRect(rectBounds, + new Paint()..shader = engineGradientToShader(sweepGradient, rectBounds)); + canvas.drawRect(rectBounds, borderPaint); + + // Tile mode mirror + rectBounds = rectBounds.translate(kBoxWidth + 10, 0); + sweepGradient = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.mirror, + math.pi / 6, 3 * math.pi / 4, + Matrix4.rotationZ(math.pi / 6.0).storage); + canvas.drawRect(rectBounds, + new Paint()..shader = engineGradientToShader(sweepGradient, rectBounds)); + canvas.drawRect(rectBounds, borderPaint); + + canvas.restore(); + await _checkScreenshot(canvas, 'sweep_gradient_rect'); + }); + + test('Paints sweep gradient ovals', () async { + final RecordingCanvas canvas = + RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); + canvas.save(); + + final Paint borderPaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 1 + ..color = Color(0xFF000000); + + List colors = [ + Color(0xFF000000), + Color(0xFFFF3C38), + Color(0xFFFF8C42), + Color(0xFFFFF275), + Color(0xFF6699CC), + Color(0xFF656D78),]; + List stops = [0.0, 0.05, 0.4, 0.6, 0.9, 1.0]; + + EngineGradient sweepGradient = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.clamp, + 0, 360.0 / 180.0 * math.pi, + Matrix4.rotationZ(math.pi / 6.0).storage); + + EngineGradient sweepGradientRotated = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.clamp, + 0, 360.0 / 180.0 * math.pi, + Matrix4.rotationZ(math.pi / 6.0).storage); + + const double kBoxWidth = 150; + const double kBoxHeight = 80; + // Gradient with default center. + Rect rectBounds = Rect.fromLTWH(10, 20, kBoxWidth, kBoxHeight); + canvas.drawOval(rectBounds, + Paint()..shader = engineGradientToShader(sweepGradient, rectBounds)); + canvas.drawRect(rectBounds, borderPaint); + + // Gradient with shifted center and rotation. + rectBounds = rectBounds.translate(kBoxWidth + 10, 0); + canvas.drawOval(rectBounds, + Paint()..shader = engineGradientToShader(sweepGradientRotated, Rect.fromLTWH(rectBounds.center.dx, rectBounds.top, rectBounds.width / 2, rectBounds.height))); + canvas.drawRect(rectBounds, borderPaint); + + // Gradient with start/endangle. + sweepGradient = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.clamp, + math.pi / 6, 3 * math.pi / 4, + Matrix4.rotationZ(math.pi / 6.0).storage); + + rectBounds = rectBounds.translate(kBoxWidth + 10, 0); + canvas.drawOval(rectBounds, + new Paint()..shader = engineGradientToShader(sweepGradient, rectBounds)); + canvas.drawRect(rectBounds, borderPaint); + + // Tile mode repeat + rectBounds = Rect.fromLTWH(10, 110, kBoxWidth, kBoxHeight); + sweepGradient = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.repeated, + math.pi / 6, 3 * math.pi / 4, + Matrix4.rotationZ(math.pi / 6.0).storage); + + canvas.drawOval(rectBounds, + new Paint()..shader = engineGradientToShader(sweepGradient, rectBounds)); + canvas.drawRect(rectBounds, borderPaint); + + // Tile mode mirror + rectBounds = rectBounds.translate(kBoxWidth + 10, 0); + sweepGradient = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.mirror, + math.pi / 6, 3 * math.pi / 4, + Matrix4.rotationZ(math.pi / 6.0).storage); + canvas.drawOval(rectBounds, + new Paint()..shader = engineGradientToShader(sweepGradient, rectBounds)); + canvas.drawRect(rectBounds, borderPaint); + + canvas.restore(); + await _checkScreenshot(canvas, 'sweep_gradient_oval'); + }); + + test('Paints sweep gradient paths', () async { + final RecordingCanvas canvas = + RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); + canvas.save(); + + final Paint borderPaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 1 + ..color = Color(0xFF000000); + + List colors = [ + Color(0xFF000000), + Color(0xFFFF3C38), + Color(0xFFFF8C42), + Color(0xFFFFF275), + Color(0xFF6699CC), + Color(0xFF656D78),]; + List stops = [0.0, 0.05, 0.4, 0.6, 0.9, 1.0]; + + EngineGradient sweepGradient = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.clamp, + 0, 360.0 / 180.0 * math.pi, + Matrix4.rotationZ(math.pi / 6.0).storage); + + EngineGradient sweepGradientRotated = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.clamp, + 0, 360.0 / 180.0 * math.pi, + Matrix4.rotationZ(math.pi / 6.0).storage); + + const double kBoxWidth = 150; + const double kBoxHeight = 80; + // Gradient with default center. + Rect rectBounds = Rect.fromLTWH(10, 20, kBoxWidth, kBoxHeight); + Path path = samplePathFromRect(rectBounds); + canvas.drawPath(path, + Paint()..shader = engineGradientToShader(sweepGradient, rectBounds)); + canvas.drawRect(rectBounds, borderPaint); + + // Gradient with shifted center and rotation. + rectBounds = rectBounds.translate(kBoxWidth + 10, 0); + path = samplePathFromRect(rectBounds); + canvas.drawPath(path, + Paint()..shader = engineGradientToShader(sweepGradientRotated, Rect.fromLTWH(rectBounds.center.dx, rectBounds.top, rectBounds.width / 2, rectBounds.height))); + canvas.drawRect(rectBounds, borderPaint); + + // Gradient with start/endangle. + sweepGradient = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.clamp, + math.pi / 6, 3 * math.pi / 4, + Matrix4.rotationZ(math.pi / 6.0).storage); + + rectBounds = rectBounds.translate(kBoxWidth + 10, 0); + path = samplePathFromRect(rectBounds); + canvas.drawPath(path, + new Paint()..shader = engineGradientToShader(sweepGradient, rectBounds)); + canvas.drawRect(rectBounds, borderPaint); + + // Tile mode repeat + rectBounds = Rect.fromLTWH(10, 110, kBoxWidth, kBoxHeight); + sweepGradient = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.repeated, + math.pi / 6, 3 * math.pi / 4, + Matrix4.rotationZ(math.pi / 6.0).storage); + + path = samplePathFromRect(rectBounds); + canvas.drawPath(path, + new Paint()..shader = engineGradientToShader(sweepGradient, rectBounds)); + canvas.drawRect(rectBounds, borderPaint); + + // Tile mode mirror + rectBounds = rectBounds.translate(kBoxWidth + 10, 0); + sweepGradient = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.mirror, + math.pi / 6, 3 * math.pi / 4, + Matrix4.rotationZ(math.pi / 6.0).storage); + path = samplePathFromRect(rectBounds); + canvas.drawPath(path, + new Paint()..shader = engineGradientToShader(sweepGradient, rectBounds)); + canvas.drawRect(rectBounds, borderPaint); + + canvas.restore(); + await _checkScreenshot(canvas, 'sweep_gradient_path'); + }); +} + +Shader engineGradientToShader(GradientSweep gradient, Rect rect) { + return Gradient.sweep( + Offset(rect.left + gradient.center.dx * rect.width, + rect.top + gradient.center.dy * rect.height), + gradient.colors, gradient.colorStops, gradient.tileMode, + gradient.startAngle, + gradient.endAngle, + gradient.matrix4 == null ? null : + Float64List.fromList(gradient.matrix4), + ); +} + +Path samplePathFromRect(Rect rectBounds) => + Path() + ..moveTo(rectBounds.center.dx, rectBounds.top) + ..lineTo(rectBounds.left, rectBounds.bottom) + ..quadraticBezierTo(rectBounds.center.dx + 20, rectBounds.bottom - 40, + rectBounds.right, rectBounds.bottom) + ..close(); diff --git a/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart b/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart index c7854c961a798..e7397545124d9 100644 --- a/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart @@ -678,7 +678,7 @@ void testMain() async { await matchGoldenFile( 'paint_spread_bounds.png', region: const Rect.fromLTRB(0, 0, 250, 600), - maxDiffRatePercent: 0.01, + maxDiffRatePercent: 0.2, pixelComparison: PixelComparison.precise, ); } finally { diff --git a/lib/web_ui/test/golden_tests/engine/screenshot.dart b/lib/web_ui/test/golden_tests/engine/screenshot.dart new file mode 100644 index 0000000000000..81cae453c2a56 --- /dev/null +++ b/lib/web_ui/test/golden_tests/engine/screenshot.dart @@ -0,0 +1,43 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.6 +import 'dart:html' as html; +import 'package:ui/ui.dart' as ui; +import 'package:ui/src/engine.dart'; +import 'package:web_engine_tester/golden_tester.dart'; +import 'package:test/test.dart'; + +/// Commit a recording canvas to a bitmap, and compare with the expected. +Future canvasScreenshot(RecordingCanvas rc, String fileName, + {ui.Rect region = const ui.Rect.fromLTWH(0, 0, 600, 800), + double maxDiffRatePercent = 0.0, bool write: false}) async { + final EngineCanvas engineCanvas = BitmapCanvas(region); + + rc.endRecording(); + rc.apply(engineCanvas, region); + + // Wrap in so that our CSS selectors kick in. + final html.Element sceneElement = html.Element.tag('flt-scene'); + try { + sceneElement.append(engineCanvas.rootElement); + html.document.body.append(sceneElement); + await matchGoldenFile('$fileName.png', + region: region, maxDiffRatePercent: maxDiffRatePercent, write: write); + } finally { + // The page is reused across tests, so remove the element after taking the + // Scuba screenshot. + sceneElement.remove(); + } +} + +/// Configures the test to use bundled Roboto and Ahem fonts to avoid golden +/// screenshot differences due to differences in the preinstalled system fonts. +void setUpStableTestFonts() { + setUp(() async { + await ui.webOnlyInitializePlatform(); + ui.webOnlyFontCollection.debugRegisterTestFonts(); + await ui.webOnlyFontCollection.ensureFontsLoaded(); + }); +} diff --git a/lib/web_ui/test/mock_engine_canvas.dart b/lib/web_ui/test/mock_engine_canvas.dart index a88dccc0b12ca..91b59ceb8e320 100644 --- a/lib/web_ui/test/mock_engine_canvas.dart +++ b/lib/web_ui/test/mock_engine_canvas.dart @@ -99,7 +99,7 @@ class MockEngineCanvas implements EngineCanvas { } @override - void clipRect(Rect rect) { + void clipRect(Rect rect, ClipOp op) { _called('clipRect', arguments: rect); } diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart index a5bc7fa21fe70..c8efd8182414d 100644 --- a/lib/web_ui/test/text_editing_test.dart +++ b/lib/web_ui/test/text_editing_test.dart @@ -1343,6 +1343,18 @@ void testMain() { '500 12px sans-serif', ); + // For `blink` and `webkit` browser engines the overlay would be hidden. + if (browserEngine == BrowserEngine.blink || + browserEngine == BrowserEngine.webkit) { + expect(textEditing.editingElement.domElement.classes, + contains('transparentTextEditing')); + } else { + expect( + textEditing.editingElement.domElement.classes.any( + (element) => element.toString() == 'transparentTextEditing'), + isFalse); + } + const MethodCall clearClient = MethodCall('TextInput.clearClient'); sendFrameworkMessage(codec.encodeMethodCall(clearClient)); }, @@ -1806,6 +1818,17 @@ void testMain() { final CssStyleDeclaration css = firstElement.style; expect(css.color, 'transparent'); expect(css.backgroundColor, 'transparent'); + + // For `blink` and `webkit` browser engines the overlay would be hidden. + if (browserEngine == BrowserEngine.blink || + browserEngine == BrowserEngine.webkit) { + expect(firstElement.classes, contains('transparentTextEditing')); + } else { + expect( + firstElement.classes.any( + (element) => element.toString() == 'transparentTextEditing'), + isFalse); + } }); test('validate multi element form ids sorted for form id', () { diff --git a/lib/web_ui/test/window_test.dart b/lib/web_ui/test/window_test.dart index b83849bffc8d6..ef0a755f550cf 100644 --- a/lib/web_ui/test/window_test.dart +++ b/lib/web_ui/test/window_test.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. // @dart = 2.6 -import 'dart:async'; import 'dart:html' as html; import 'dart:js_util' as js_util; import 'dart:typed_data'; @@ -12,34 +11,39 @@ import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; -const MethodCodec codec = JSONMethodCodec(); +import 'engine/history_test.dart'; +import 'matchers.dart'; -void emptyCallback(ByteData date) {} +const MethodCodec codec = JSONMethodCodec(); -Future setStrategy(TestLocationStrategy newStrategy) async { - await window.browserHistory.setLocationStrategy(newStrategy); -} +void emptyCallback(ByteData data) {} void main() { internalBootstrapBrowserTest(() => testMain); } void testMain() { - setUp(() async { - await window.debugSwitchBrowserHistory(useSingle: true); + tearDown(() async { + await window.debugResetHistory(); }); test('window.defaultRouteName should not change', () async { - await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/initial'))); + final TestUrlStrategy strategy = TestUrlStrategy.fromEntry( + TestHistoryEntry('initial state', null, '/initial'), + ); + await window.debugInitializeHistory(strategy, useSingle: true); expect(window.defaultRouteName, '/initial'); // Changing the URL in the address bar later shouldn't affect [window.defaultRouteName]. - window.locationStrategy.replaceState(null, null, '/newpath'); + strategy.replaceState(null, null, '/newpath'); expect(window.defaultRouteName, '/initial'); }); - test('window.defaultRouteName should reset after navigation platform message', () async { - await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/initial'))); + test('window.defaultRouteName should reset after navigation platform message', + () async { + await window.debugInitializeHistory(TestUrlStrategy.fromEntry( + TestHistoryEntry('initial state', null, '/initial'), + ), useSingle: true); // Reading it multiple times should return the same value. expect(window.defaultRouteName, '/initial'); expect(window.defaultRouteName, '/initial'); @@ -57,45 +61,45 @@ void testMain() { }); test('can disable location strategy', () async { - await window.debugSwitchBrowserHistory(useSingle: true); - final testStrategy = TestLocationStrategy.fromEntry( + // Disable URL strategy. + expect(() => jsSetUrlStrategy(null), returnsNormally); + // History should be initialized. + expect(window.browserHistory, isNotNull); + // But without a URL strategy. + expect(window.browserHistory.urlStrategy, isNull); + // Current path is always "/" in this case. + expect(window.browserHistory.currentPath, '/'); + + // Perform some navigation operations. + routeInfomrationUpdated('/foo/bar', null); + // Path should not be updated because URL strategy is disabled. + expect(window.browserHistory.currentPath, '/'); + }); + + test('js interop throws on wrong type', () { + expect(() => jsSetUrlStrategy(123), throwsA(anything)); + expect(() => jsSetUrlStrategy('foo'), throwsA(anything)); + expect(() => jsSetUrlStrategy(false), throwsA(anything)); + }); + + test('cannot set url strategy after it is initialized', () async { + final testStrategy = TestUrlStrategy.fromEntry( TestHistoryEntry('initial state', null, '/'), ); - await setStrategy(testStrategy); - - expect(window.locationStrategy, testStrategy); - // A single listener should've been setup. - expect(testStrategy.listeners, hasLength(1)); - // The initial entry should be there, plus another "flutter" entry. - expect(testStrategy.history, hasLength(2)); - expect(testStrategy.history[0].state, {'origin': true, 'state': 'initial state'}); - expect(testStrategy.history[1].state, {'flutter': true}); - expect(testStrategy.currentEntry, testStrategy.history[1]); - - // Now, let's disable location strategy and make sure things get cleaned up. - expect(() => jsSetLocationStrategy(null), returnsNormally); - // The locationStrategy is teared down asynchronously. - await Future.delayed(Duration.zero); - expect(window.locationStrategy, isNull); - - // The listener is removed asynchronously. - await Future.delayed(const Duration(milliseconds: 10)); - - // No more listeners. - expect(testStrategy.listeners, isEmpty); - // History should've moved back to the initial state. - expect(testStrategy.history[0].state, "initial state"); - expect(testStrategy.currentEntry, testStrategy.history[0]); + await window.debugInitializeHistory(testStrategy, useSingle: true); + + expect(() => jsSetUrlStrategy(null), throwsA(isAssertionError)); }); - test('js interop throws on wrong type', () { - expect(() => jsSetLocationStrategy(123), throwsA(anything)); - expect(() => jsSetLocationStrategy('foo'), throwsA(anything)); - expect(() => jsSetLocationStrategy(false), throwsA(anything)); + test('cannot set url strategy more than once', () async { + // First time is okay. + expect(() => jsSetUrlStrategy(null), returnsNormally); + // Second time is not allowed. + expect(() => jsSetUrlStrategy(null), throwsA(isAssertionError)); }); } -void jsSetLocationStrategy(dynamic strategy) { +void jsSetUrlStrategy(dynamic strategy) { js_util.callMethod( html.window, '_flutter_web_set_location_strategy', diff --git a/runtime/BUILD.gn b/runtime/BUILD.gn index d584693fdcfe7..9788794f7da83 100644 --- a/runtime/BUILD.gn +++ b/runtime/BUILD.gn @@ -53,6 +53,8 @@ source_set("runtime") { "dart_vm_lifecycle.h", "embedder_resources.cc", "embedder_resources.h", + "isolate_configuration.cc", + "isolate_configuration.h", "platform_data.cc", "platform_data.h", "ptrace_check.h", @@ -112,6 +114,7 @@ if (enable_unittests) { "dart_lifecycle_unittests.cc", "dart_service_isolate_unittests.cc", "dart_vm_unittests.cc", + "type_conversions_unittests.cc", ] public_configs = [ "//flutter:export_dynamic_symbols" ] diff --git a/runtime/dart_isolate.cc b/runtime/dart_isolate.cc index 919a9a532c17a..b834957a9281a 100644 --- a/runtime/dart_isolate.cc +++ b/runtime/dart_isolate.cc @@ -17,6 +17,7 @@ #include "flutter/runtime/dart_service_isolate.h" #include "flutter/runtime/dart_vm.h" #include "flutter/runtime/dart_vm_lifecycle.h" +#include "flutter/runtime/isolate_configuration.h" #include "third_party/dart/runtime/include/dart_api.h" #include "third_party/dart/runtime/include/dart_tools_api.h" #include "third_party/tonic/converter/dart_converter.h" @@ -52,6 +53,126 @@ class DartErrorString { } // anonymous namespace +DartIsolate::Flags::Flags() : Flags(nullptr) {} + +DartIsolate::Flags::Flags(const Dart_IsolateFlags* flags) { + if (flags) { + flags_ = *flags; + } else { + ::Dart_IsolateFlagsInitialize(&flags_); + } +} + +DartIsolate::Flags::~Flags() = default; + +void DartIsolate::Flags::SetNullSafetyEnabled(bool enabled) { + flags_.null_safety = enabled; +} + +Dart_IsolateFlags DartIsolate::Flags::Get() const { + return flags_; +} + +std::weak_ptr DartIsolate::CreateRunningRootIsolate( + const Settings& settings, + fml::RefPtr isolate_snapshot, + TaskRunners task_runners, + std::unique_ptr platform_configuration, + fml::WeakPtr snapshot_delegate, + fml::WeakPtr hint_freed_delegate, + fml::WeakPtr io_manager, + fml::RefPtr skia_unref_queue, + fml::WeakPtr image_decoder, + std::string advisory_script_uri, + std::string advisory_script_entrypoint, + Flags isolate_flags, + const fml::closure& isolate_create_callback, + const fml::closure& isolate_shutdown_callback, + std::optional dart_entrypoint, + std::optional dart_entrypoint_library, + std::unique_ptr isolate_configration) { + if (!isolate_snapshot) { + FML_LOG(ERROR) << "Invalid isolate snapshot."; + return {}; + } + + if (!isolate_configration) { + FML_LOG(ERROR) << "Invalid isolate configuration."; + return {}; + } + + isolate_flags.SetNullSafetyEnabled( + isolate_configration->IsNullSafetyEnabled(*isolate_snapshot)); + + auto isolate = CreateRootIsolate(settings, // + isolate_snapshot, // + task_runners, // + std::move(platform_configuration), // + snapshot_delegate, // + hint_freed_delegate, // + io_manager, // + skia_unref_queue, // + image_decoder, // + advisory_script_uri, // + advisory_script_entrypoint, // + isolate_flags, // + isolate_create_callback, // + isolate_shutdown_callback // + ) + .lock(); + + if (!isolate) { + FML_LOG(ERROR) << "Could not create root isolate."; + return {}; + } + + fml::ScopedCleanupClosure shutdown_on_error([isolate]() { + if (!isolate->Shutdown()) { + FML_DLOG(ERROR) << "Could not shutdown transient isolate."; + } + }); + + if (isolate->GetPhase() != DartIsolate::Phase::LibrariesSetup) { + FML_LOG(ERROR) << "Root isolate was created in an incorrect phase."; + return {}; + } + + if (!isolate_configration->PrepareIsolate(*isolate.get())) { + FML_LOG(ERROR) << "Could not prepare isolate."; + return {}; + } + + if (isolate->GetPhase() != DartIsolate::Phase::Ready) { + FML_LOG(ERROR) << "Root isolate not in the ready phase for Dart entrypoint " + "invocation."; + return {}; + } + + if (settings.root_isolate_create_callback) { + // Isolate callbacks always occur in isolate scope and before user code has + // had a chance to run. + tonic::DartState::Scope scope(isolate.get()); + settings.root_isolate_create_callback(*isolate.get()); + } + + if (!isolate->RunFromLibrary(dart_entrypoint_library, // + dart_entrypoint, // + settings.dart_entrypoint_args // + )) { + FML_LOG(ERROR) << "Could not run the run main Dart entrypoint."; + return {}; + } + + if (settings.root_isolate_shutdown_callback) { + isolate->AddIsolateShutdownCallback( + settings.root_isolate_shutdown_callback); + } + + shutdown_on_error.Release(); + + return isolate; +} + std::weak_ptr DartIsolate::CreateRootIsolate( const Settings& settings, fml::RefPtr isolate_snapshot, @@ -64,7 +185,7 @@ std::weak_ptr DartIsolate::CreateRootIsolate( fml::WeakPtr image_decoder, std::string advisory_script_uri, std::string advisory_script_entrypoint, - Dart_IsolateFlags* flags, + Flags flags, const fml::closure& isolate_create_callback, const fml::closure& isolate_shutdown_callback) { TRACE_EVENT0("flutter", "DartIsolate::CreateRootIsolate"); @@ -98,9 +219,10 @@ std::weak_ptr DartIsolate::CreateRootIsolate( ))); DartErrorString error; - Dart_Isolate vm_isolate = - CreateDartIsolateGroup(std::move(isolate_group_data), - std::move(isolate_data), flags, error.error()); + auto isolate_flags = flags.Get(); + Dart_Isolate vm_isolate = CreateDartIsolateGroup( + std::move(isolate_group_data), std::move(isolate_data), &isolate_flags, + error.error()); if (error) { FML_LOG(ERROR) << "CreateDartIsolateGroup failed: " << error.str(); @@ -493,40 +615,9 @@ bool DartIsolate::MarkIsolateRunnable() { return true; } -/// @note Procedure doesn't copy all closures. -[[nodiscard]] bool DartIsolate::Run(const std::string& entrypoint_name, - const std::vector& args, - const fml::closure& on_run) { - TRACE_EVENT0("flutter", "DartIsolate::Run"); - if (phase_ != Phase::Ready) { - return false; - } - - tonic::DartState::Scope scope(this); - - auto user_entrypoint_function = - Dart_GetField(Dart_RootLibrary(), tonic::ToDart(entrypoint_name.c_str())); - - auto entrypoint_args = tonic::ToDart(args); - - if (!InvokeMainEntrypoint(user_entrypoint_function, entrypoint_args)) { - return false; - } - - phase_ = Phase::Running; - - if (on_run) { - on_run(); - } - return true; -} - -/// @note Procedure doesn't copy all closures. -[[nodiscard]] bool DartIsolate::RunFromLibrary( - const std::string& library_name, - const std::string& entrypoint_name, - const std::vector& args, - const fml::closure& on_run) { +bool DartIsolate::RunFromLibrary(std::optional library_name, + std::optional entrypoint, + const std::vector& args) { TRACE_EVENT0("flutter", "DartIsolate::RunFromLibrary"); if (phase_ != Phase::Ready) { return false; @@ -534,9 +625,15 @@ bool DartIsolate::MarkIsolateRunnable() { tonic::DartState::Scope scope(this); + auto library_handle = + library_name.has_value() && !library_name.value().empty() + ? ::Dart_LookupLibrary(tonic::ToDart(library_name.value().c_str())) + : ::Dart_RootLibrary(); + auto entrypoint_handle = entrypoint.has_value() && !entrypoint.value().empty() + ? tonic::ToDart(entrypoint.value().c_str()) + : tonic::ToDart("main"); auto user_entrypoint_function = - Dart_GetField(Dart_LookupLibrary(tonic::ToDart(library_name.c_str())), - tonic::ToDart(entrypoint_name.c_str())); + ::Dart_GetField(library_handle, entrypoint_handle); auto entrypoint_args = tonic::ToDart(args); @@ -546,9 +643,6 @@ bool DartIsolate::MarkIsolateRunnable() { phase_ = Phase::Running; - if (on_run) { - on_run(); - } return true; } @@ -600,6 +694,14 @@ Dart_Isolate DartIsolate::DartCreateAndStartServiceIsolate( flags->load_vmservice_library = true; +#if (FLUTTER_RUNTIME_MODE != FLUTTER_RUNTIME_MODE_DEBUG) + // TODO(68663): The service isolate in debug mode is always launched without + // sound null safety. Fix after the isolate snapshot data is created with the + // right flags. + flags->null_safety = + vm_data->GetIsolateSnapshot()->IsNullSafetyEnabled(nullptr); +#endif + std::weak_ptr weak_service_isolate = DartIsolate::CreateRootIsolate( vm_data->GetSettings(), // settings @@ -613,7 +715,7 @@ Dart_Isolate DartIsolate::DartCreateAndStartServiceIsolate( {}, // Image Decoder DART_VM_SERVICE_ISOLATE_NAME, // script uri DART_VM_SERVICE_ISOLATE_NAME, // script entrypoint - flags, // flags + DartIsolate::Flags{flags}, // flags nullptr, // isolate create callback nullptr // isolate shutdown callback ); @@ -641,6 +743,10 @@ Dart_Isolate DartIsolate::DartCreateAndStartServiceIsolate( return nullptr; } + if (auto callback = vm_data->GetSettings().service_isolate_create_callback) { + callback(); + } + if (auto service_protocol = DartVMRef::GetServiceProtocol()) { service_protocol->ToggleHooks(true); } else { diff --git a/runtime/dart_isolate.h b/runtime/dart_isolate.h index d1f741518ec21..5e6913d52b185 100644 --- a/runtime/dart_isolate.h +++ b/runtime/dart_isolate.h @@ -6,6 +6,7 @@ #define FLUTTER_RUNTIME_DART_ISOLATE_H_ #include +#include #include #include @@ -26,6 +27,7 @@ namespace flutter { class DartVM; class DartIsolateGroupData; +class IsolateConfiguration; //------------------------------------------------------------------------------ /// @brief Represents an instance of a live isolate. An isolate is a @@ -59,6 +61,22 @@ class DartIsolateGroupData; /// class DartIsolate : public UIDartState { public: + class Flags { + public: + Flags(); + + explicit Flags(const Dart_IsolateFlags* flags); + + ~Flags(); + + void SetNullSafetyEnabled(bool enabled); + + Dart_IsolateFlags Get() const; + + private: + Dart_IsolateFlags flags_; + }; + //---------------------------------------------------------------------------- /// @brief The engine represents all dart isolates as being in one of the /// known phases. By invoking various methods on the Dart isolate, @@ -174,14 +192,19 @@ class DartIsolate : public UIDartState { /// the root isolate. The isolate is /// already in the running state at /// this point and an isolate scope is - /// current. + /// current. This callback is made for + /// all isolate launches (including + /// the children of the root isolate). /// @param[in] isolate_shutdown_callback The isolate shutdown callback. /// This will be called before the /// isolate is about to transition /// into the Shutdown phase. The /// isolate is still running at this /// point and an isolate scope is - /// current. + /// current. This callback is made + /// for all isolate shutdowns + /// (including the children of the + /// root isolate). /// /// @return A weak pointer to the root Dart isolate. The caller must /// ensure that the isolate is not referenced for long periods of @@ -189,7 +212,7 @@ class DartIsolate : public UIDartState { /// terminates itself. The caller may also only use the isolate on /// the thread on which the isolate was created. /// - static std::weak_ptr CreateRootIsolate( + static std::weak_ptr CreateRunningRootIsolate( const Settings& settings, fml::RefPtr isolate_snapshot, TaskRunners task_runners, @@ -201,9 +224,12 @@ class DartIsolate : public UIDartState { fml::WeakPtr image_decoder, std::string advisory_script_uri, std::string advisory_script_entrypoint, - Dart_IsolateFlags* flags, + Flags flags, const fml::closure& isolate_create_callback, - const fml::closure& isolate_shutdown_callback); + const fml::closure& isolate_shutdown_callback, + std::optional dart_entrypoint, + std::optional dart_entrypoint_library, + std::unique_ptr isolate_configration); // |UIDartState| ~DartIsolate() override; @@ -306,26 +332,6 @@ class DartIsolate : public UIDartState { [[nodiscard]] bool PrepareForRunningFromKernels( std::vector> kernels); - //---------------------------------------------------------------------------- - /// @brief Transition the root isolate to the `Phase::Running` phase and - /// invoke the main entrypoint (the "main" method) in the root - /// library. The isolate must already be in the `Phase::Ready` - /// phase. - /// - /// @param[in] entrypoint The entrypoint in the root library. - /// @param[in] args A list of string arguments to the entrypoint. - /// @param[in] on_run A callback to run in isolate scope after the main - /// entrypoint has been invoked. There is no isolate - /// scope current on the thread once this method - /// returns. - /// - /// @return If the isolate successfully transitioned to the running phase - /// and the main entrypoint was invoked. - /// - [[nodiscard]] bool Run(const std::string& entrypoint, - const std::vector& args, - const fml::closure& on_run = nullptr); - //---------------------------------------------------------------------------- /// @brief Transition the root isolate to the `Phase::Running` phase and /// invoke the main entrypoint (the "main" method) in the @@ -336,18 +342,13 @@ class DartIsolate : public UIDartState { /// supplied entrypoint. /// @param[in] entrypoint The entrypoint in `library_name` /// @param[in] args A list of string arguments to the entrypoint. - /// @param[in] on_run A callback to run in isolate scope after the - /// main entrypoint has been invoked. There is no - /// isolate scope current on the thread once this - /// method returns. /// /// @return If the isolate successfully transitioned to the running phase /// and the main entrypoint was invoked. /// - [[nodiscard]] bool RunFromLibrary(const std::string& library_name, - const std::string& entrypoint, - const std::vector& args, - const fml::closure& on_run = nullptr); + [[nodiscard]] bool RunFromLibrary(std::optional library_name, + std::optional entrypoint, + const std::vector& args); //---------------------------------------------------------------------------- /// @brief Transition the isolate to the `Phase::Shutdown` phase. The @@ -384,6 +385,7 @@ class DartIsolate : public UIDartState { fml::RefPtr GetMessageHandlingTaskRunner() const; private: + friend class IsolateConfiguration; class AutoFireClosure { public: AutoFireClosure(const fml::closure& closure); @@ -403,6 +405,22 @@ class DartIsolate : public UIDartState { const bool may_insecurely_connect_to_all_domains_; std::string domain_network_policy_; + static std::weak_ptr CreateRootIsolate( + const Settings& settings, + fml::RefPtr isolate_snapshot, + TaskRunners task_runners, + std::unique_ptr platform_configuration, + fml::WeakPtr snapshot_delegate, + fml::WeakPtr hint_freed_delegate, + fml::WeakPtr io_manager, + fml::RefPtr skia_unref_queue, + fml::WeakPtr image_decoder, + std::string advisory_script_uri, + std::string advisory_script_entrypoint, + Flags flags, + const fml::closure& isolate_create_callback, + const fml::closure& isolate_shutdown_callback); + DartIsolate(const Settings& settings, TaskRunners task_runners, fml::WeakPtr snapshot_delegate, @@ -413,6 +431,7 @@ class DartIsolate : public UIDartState { std::string advisory_script_uri, std::string advisory_script_entrypoint, bool is_root_isolate); + [[nodiscard]] bool Initialize(Dart_Isolate isolate); void SetMessageHandlingTaskRunner(fml::RefPtr runner); diff --git a/runtime/dart_isolate_unittests.cc b/runtime/dart_isolate_unittests.cc index 8a1bcb2cf2469..a90599fd8a3fb 100644 --- a/runtime/dart_isolate_unittests.cc +++ b/runtime/dart_isolate_unittests.cc @@ -10,6 +10,7 @@ #include "flutter/fml/thread.h" #include "flutter/runtime/dart_vm.h" #include "flutter/runtime/dart_vm_lifecycle.h" +#include "flutter/runtime/isolate_configuration.h" #include "flutter/testing/dart_isolate_runner.h" #include "flutter/testing/fixture_test.h" #include "flutter/testing/testing.h" @@ -46,25 +47,32 @@ TEST_F(DartIsolateTest, RootIsolateCreationAndShutdown) { GetCurrentTaskRunner(), // GetCurrentTaskRunner() // ); - auto weak_isolate = DartIsolate::CreateRootIsolate( - vm_data->GetSettings(), // settings - vm_data->GetIsolateSnapshot(), // isolate snapshot - std::move(task_runners), // task runners - nullptr, // window - {}, // snapshot delegate - {}, // hint freed delegate - {}, // io manager - {}, // unref queue - {}, // image decoder - "main.dart", // advisory uri - "main", // advisory entrypoint, - nullptr, // flags - settings.isolate_create_callback, // isolate create callback - settings.isolate_shutdown_callback // isolate shutdown callback + + auto isolate_configuration = + IsolateConfiguration::InferFromSettings(settings); + + auto weak_isolate = DartIsolate::CreateRunningRootIsolate( + vm_data->GetSettings(), // settings + vm_data->GetIsolateSnapshot(), // isolate snapshot + std::move(task_runners), // task runners + nullptr, // window + {}, // snapshot delegate + {}, // hint freed delegate + {}, // io manager + {}, // unref queue + {}, // image decoder + "main.dart", // advisory uri + "main", // advisory entrypoint, + DartIsolate::Flags{}, // flags + settings.isolate_create_callback, // isolate create callback + settings.isolate_shutdown_callback, // isolate shutdown callback + "main", // dart entrypoint + std::nullopt, // dart entrypoint library + std::move(isolate_configuration) // isolate configuration ); auto root_isolate = weak_isolate.lock(); ASSERT_TRUE(root_isolate); - ASSERT_EQ(root_isolate->GetPhase(), DartIsolate::Phase::LibrariesSetup); + ASSERT_EQ(root_isolate->GetPhase(), DartIsolate::Phase::Running); ASSERT_TRUE(root_isolate->Shutdown()); } @@ -81,25 +89,30 @@ TEST_F(DartIsolateTest, IsolateShutdownCallbackIsInIsolateScope) { GetCurrentTaskRunner(), // GetCurrentTaskRunner() // ); - auto weak_isolate = DartIsolate::CreateRootIsolate( - vm_data->GetSettings(), // settings - vm_data->GetIsolateSnapshot(), // isolate snapshot - std::move(task_runners), // task runners - nullptr, // window - {}, // snapshot delegate - {}, // hint freed delegate - {}, // io manager - {}, // unref queue - {}, // image decoder - "main.dart", // advisory uri - "main", // advisory entrypoint - nullptr, // flags - settings.isolate_create_callback, // isolate create callback - settings.isolate_shutdown_callback // isolate shutdown callback + auto isolate_configuration = + IsolateConfiguration::InferFromSettings(settings); + auto weak_isolate = DartIsolate::CreateRunningRootIsolate( + vm_data->GetSettings(), // settings + vm_data->GetIsolateSnapshot(), // isolate snapshot + std::move(task_runners), // task runners + nullptr, // window + {}, // snapshot delegate + {}, // hint freed delegate + {}, // io manager + {}, // unref queue + {}, // image decoder + "main.dart", // advisory uri + "main", // advisory entrypoint + DartIsolate::Flags{}, // flags + settings.isolate_create_callback, // isolate create callback + settings.isolate_shutdown_callback, // isolate shutdown callback + "main", // dart entrypoint + std::nullopt, // dart entrypoint library + std::move(isolate_configuration) // isolate configuration ); auto root_isolate = weak_isolate.lock(); ASSERT_TRUE(root_isolate); - ASSERT_EQ(root_isolate->GetPhase(), DartIsolate::Phase::LibrariesSetup); + ASSERT_EQ(root_isolate->GetPhase(), DartIsolate::Phase::Running); size_t destruction_callback_count = 0; root_isolate->AddIsolateShutdownCallback([&destruction_callback_count]() { ASSERT_NE(Dart_CurrentIsolate(), nullptr); @@ -308,5 +321,114 @@ TEST_F(DartIsolateTest, CanRecieveArguments) { Wait(); } +TEST_F(DartIsolateTest, CanCreateServiceIsolate) { +#if (FLUTTER_RUNTIME_MODE != FLUTTER_RUNTIME_MODE_DEBUG) && \ + (FLUTTER_RUNTIME_MODE != FLUTTER_RUNTIME_MODE_PROFILE) + GTEST_SKIP(); +#endif + ASSERT_FALSE(DartVMRef::IsInstanceRunning()); + fml::AutoResetWaitableEvent service_isolate_latch; + auto settings = CreateSettingsForFixture(); + settings.enable_observatory = true; + settings.observatory_port = 0; + settings.observatory_host = "127.0.0.1"; + settings.enable_service_port_fallback = true; + settings.service_isolate_create_callback = [&service_isolate_latch]() { + service_isolate_latch.Signal(); + }; + auto vm_ref = DartVMRef::Create(settings); + ASSERT_TRUE(vm_ref); + auto vm_data = vm_ref.GetVMData(); + ASSERT_TRUE(vm_data); + TaskRunners task_runners(GetCurrentTestName(), // + GetCurrentTaskRunner(), // + GetCurrentTaskRunner(), // + GetCurrentTaskRunner(), // + GetCurrentTaskRunner() // + ); + + auto isolate_configuration = + IsolateConfiguration::InferFromSettings(settings); + auto weak_isolate = DartIsolate::CreateRunningRootIsolate( + vm_data->GetSettings(), // settings + vm_data->GetIsolateSnapshot(), // isolate snapshot + std::move(task_runners), // task runners + nullptr, // window + {}, // snapshot delegate + {}, // hint freed delegate + {}, // io manager + {}, // unref queue + {}, // image decoder + "main.dart", // advisory uri + "main", // advisory entrypoint, + DartIsolate::Flags{}, // flags + settings.isolate_create_callback, // isolate create callback + settings.isolate_shutdown_callback, // isolate shutdown callback + "main", // dart entrypoint + std::nullopt, // dart entrypoint library + std::move(isolate_configuration) // isolate configuration + ); + auto root_isolate = weak_isolate.lock(); + ASSERT_TRUE(root_isolate); + ASSERT_EQ(root_isolate->GetPhase(), DartIsolate::Phase::Running); + service_isolate_latch.Wait(); + ASSERT_TRUE(root_isolate->Shutdown()); +} + +TEST_F(DartIsolateTest, + RootIsolateCreateCallbackIsMadeOnceAndBeforeIsolateRunning) { + ASSERT_FALSE(DartVMRef::IsInstanceRunning()); + auto settings = CreateSettingsForFixture(); + size_t create_callback_count = 0u; + settings.root_isolate_create_callback = + [&create_callback_count](const auto& isolate) { + ASSERT_EQ(isolate.GetPhase(), DartIsolate::Phase::Ready); + create_callback_count++; + ASSERT_NE(::Dart_CurrentIsolate(), nullptr); + }; + auto vm_ref = DartVMRef::Create(settings); + TaskRunners task_runners(GetCurrentTestName(), // + GetCurrentTaskRunner(), // + GetCurrentTaskRunner(), // + GetCurrentTaskRunner(), // + GetCurrentTaskRunner() // + ); + { + auto isolate = RunDartCodeInIsolate(vm_ref, settings, task_runners, "main", + {}, GetFixturesPath()); + ASSERT_TRUE(isolate); + ASSERT_EQ(isolate->get()->GetPhase(), DartIsolate::Phase::Running); + } + ASSERT_EQ(create_callback_count, 1u); +} + +TEST_F(DartIsolateTest, + IsolateCreateCallbacksTakeInstanceSettingsInsteadOfVMSettings) { + ASSERT_FALSE(DartVMRef::IsInstanceRunning()); + auto vm_settings = CreateSettingsForFixture(); + auto vm_ref = DartVMRef::Create(vm_settings); + auto instance_settings = vm_settings; + size_t create_callback_count = 0u; + instance_settings.root_isolate_create_callback = + [&create_callback_count](const auto& isolate) { + ASSERT_EQ(isolate.GetPhase(), DartIsolate::Phase::Ready); + create_callback_count++; + ASSERT_NE(::Dart_CurrentIsolate(), nullptr); + }; + TaskRunners task_runners(GetCurrentTestName(), // + GetCurrentTaskRunner(), // + GetCurrentTaskRunner(), // + GetCurrentTaskRunner(), // + GetCurrentTaskRunner() // + ); + { + auto isolate = RunDartCodeInIsolate(vm_ref, instance_settings, task_runners, + "main", {}, GetFixturesPath()); + ASSERT_TRUE(isolate); + ASSERT_EQ(isolate->get()->GetPhase(), DartIsolate::Phase::Running); + } + ASSERT_EQ(create_callback_count, 1u); +} + } // namespace testing } // namespace flutter diff --git a/runtime/dart_lifecycle_unittests.cc b/runtime/dart_lifecycle_unittests.cc index 3c45c702a80fe..8576b3fb11292 100644 --- a/runtime/dart_lifecycle_unittests.cc +++ b/runtime/dart_lifecycle_unittests.cc @@ -8,6 +8,7 @@ #include "flutter/fml/synchronization/waitable_event.h" #include "flutter/runtime/dart_vm.h" #include "flutter/runtime/dart_vm_lifecycle.h" +#include "flutter/runtime/isolate_configuration.h" #include "flutter/testing/fixture_test.h" namespace flutter { @@ -50,57 +51,34 @@ static std::shared_ptr CreateAndRunRootIsolate( FML_CHECK(entrypoint.size() > 0); TaskRunners runners("io.flutter.test", task_runner, task_runner, task_runner, task_runner); - auto isolate_weak = DartIsolate::CreateRootIsolate( - vm.GetSettings(), // settings - vm.GetIsolateSnapshot(), // isolate_snapshot - runners, // task_runners - {}, // window - {}, // snapshot_delegate - {}, // hint_freed_delegate - {}, // io_manager - {}, // unref_queue - {}, // image_decoder - "main.dart", // advisory_script_uri - entrypoint.c_str(), // advisory_script_entrypoint - nullptr, // flags - settings.isolate_create_callback, // isolate create callback - settings.isolate_shutdown_callback // isolate shutdown callback - ); - - auto isolate = isolate_weak.lock(); - if (!isolate) { - FML_LOG(ERROR) << "Could not create valid isolate."; - return nullptr; - } - - if (DartVM::IsRunningPrecompiledCode()) { - if (!isolate->PrepareForRunningFromPrecompiledCode()) { - FML_LOG(ERROR) - << "Could not prepare to run the isolate from precompiled code."; - return nullptr; - } - - } else { - if (!isolate->PrepareForRunningFromKernels( - settings.application_kernels())) { - FML_LOG(ERROR) << "Could not prepare isolate from application kernels."; - return nullptr; - } - } + auto isolate_configuration = + IsolateConfiguration::InferFromSettings(settings); + + auto isolate = + DartIsolate::CreateRunningRootIsolate( + vm.GetSettings(), // settings + vm.GetIsolateSnapshot(), // isolate_snapshot + runners, // task_runners + {}, // window + {}, // snapshot_delegate + {}, // hint_freed_delegate + {}, // io_manager + {}, // unref_queue + {}, // image_decoder + "main.dart", // advisory_script_uri + entrypoint.c_str(), // advisory_script_entrypoint + DartIsolate::Flags{}, // flags + settings.isolate_create_callback, // isolate create callback + settings.isolate_shutdown_callback, // isolate shutdown callback, + entrypoint, // dart entrypoint + std::nullopt, // dart entrypoint library + std::move(isolate_configuration) // isolate configuration + ) + .lock(); - if (isolate->GetPhase() != DartIsolate::Phase::Ready) { - FML_LOG(ERROR) << "Isolate was not ready."; - return nullptr; - } - - if (!isolate->Run(entrypoint, {}, settings.root_isolate_create_callback)) { - FML_LOG(ERROR) << "Could not run entrypoint: " << entrypoint << "."; - return nullptr; - } - - if (isolate->GetPhase() != DartIsolate::Phase::Running) { - FML_LOG(ERROR) << "Isolate was not Running."; + if (!isolate) { + FML_LOG(ERROR) << "Could not launch the root isolate."; return nullptr; } diff --git a/runtime/dart_snapshot.cc b/runtime/dart_snapshot.cc index 050592d04a967..e9ed35418880a 100644 --- a/runtime/dart_snapshot.cc +++ b/runtime/dart_snapshot.cc @@ -11,6 +11,7 @@ #include "flutter/fml/trace_event.h" #include "flutter/lib/snapshot/snapshot.h" #include "flutter/runtime/dart_vm.h" +#include "third_party/dart/runtime/include/dart_api.h" namespace flutter { @@ -202,4 +203,16 @@ const uint8_t* DartSnapshot::GetInstructionsMapping() const { return instructions_ ? instructions_->GetMapping() : nullptr; } +bool DartSnapshot::IsNullSafetyEnabled(const fml::Mapping* kernel) const { + return ::Dart_DetectNullSafety( + nullptr, // script_uri (unsupported by Flutter) + nullptr, // package_config (package resolution of parent used) + nullptr, // original_working_directory (no package config) + GetDataMapping(), // snapshot_data + GetInstructionsMapping(), // snapshot_instructions + kernel ? kernel->GetMapping() : nullptr, // kernel_buffer + kernel ? kernel->GetSize() : 0u // kernel_buffer_size + ); +} + } // namespace flutter diff --git a/runtime/dart_snapshot.h b/runtime/dart_snapshot.h index 97038aac4aee9..15ca157c7a273 100644 --- a/runtime/dart_snapshot.h +++ b/runtime/dart_snapshot.h @@ -139,6 +139,9 @@ class DartSnapshot : public fml::RefCountedThreadSafe { /// const uint8_t* GetInstructionsMapping() const; + bool IsNullSafetyEnabled( + const fml::Mapping* application_kernel_mapping) const; + private: std::shared_ptr data_; std::shared_ptr instructions_; diff --git a/runtime/dart_vm.cc b/runtime/dart_vm.cc index 06916b5e46acd..39bcd7b606972 100644 --- a/runtime/dart_vm.cc +++ b/runtime/dart_vm.cc @@ -59,7 +59,8 @@ static const char* kDartLanguageArgs[] = { // clang-format off "--enable_mirrors=false", "--background_compilation", - "--causal_async_stacks", + "--no-causal_async_stacks", + "--lazy_async_stacks", // clang-format on }; diff --git a/runtime/fixtures/runtime_test.dart b/runtime/fixtures/runtime_test.dart index 821e01e8fa433..616e026a5ccce 100644 --- a/runtime/fixtures/runtime_test.dart +++ b/runtime/fixtures/runtime_test.dart @@ -32,13 +32,7 @@ void testIsolateShutdown() { } @pragma('vm:entry-point') void testCanSaveCompilationTrace() { - List trace; - try { - trace = saveCompilationTrace(); - } catch (exception) { - print('Could not save compilation trace: ' + exception); - } - notifyResult(trace != null && trace.isNotEmpty); + notifyResult(saveCompilationTrace().isNotEmpty); } void notifyResult(bool success) native 'NotifyNative'; @@ -58,5 +52,44 @@ void testCanLaunchSecondaryIsolate() { @pragma('vm:entry-point') void testCanRecieveArguments(List args) { - notifyResult(args != null && args.length == 1 && args[0] == 'arg1'); + notifyResult(args.length == 1 && args[0] == 'arg1'); +} + +@pragma('vm:entry-point') +void trampoline() { + notifyNative(); +} + +void notifySuccess(bool success) native 'NotifySuccess'; + +@pragma('vm:entry-point') +void testCanConvertEmptyList(List args){ + notifySuccess(args.length == 0); +} + +@pragma('vm:entry-point') +void testCanConvertListOfStrings(List args){ + notifySuccess(args.length == 4 && + args[0] == 'tinker' && + args[1] == 'tailor' && + args[2] == 'soldier' && + args[3] == 'sailor'); +} + +@pragma('vm:entry-point') +void testCanConvertListOfDoubles(List args){ + notifySuccess(args.length == 4 && + args[0] == 1.0 && + args[1] == 2.0 && + args[2] == 3.0 && + args[3] == 4.0); +} + +@pragma('vm:entry-point') +void testCanConvertListOfInts(List args){ + notifySuccess(args.length == 4 && + args[0] == 1 && + args[1] == 2 && + args[2] == 3 && + args[3] == 4); } diff --git a/shell/common/isolate_configuration.cc b/runtime/isolate_configuration.cc similarity index 70% rename from shell/common/isolate_configuration.cc rename to runtime/isolate_configuration.cc index 55b939441bd0d..a7f4aa19da5c9 100644 --- a/shell/common/isolate_configuration.cc +++ b/runtime/isolate_configuration.cc @@ -1,9 +1,8 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT -#include "flutter/shell/common/isolate_configuration.h" +#include "flutter/runtime/isolate_configuration.h" #include "flutter/fml/make_copyable.h" #include "flutter/runtime/dart_vm.h" @@ -33,6 +32,11 @@ class AppSnapshotIsolateConfiguration final : public IsolateConfiguration { return isolate.PrepareForRunningFromPrecompiledCode(); } + // |IsolateConfiguration| + bool IsNullSafetyEnabled(const DartSnapshot& snapshot) override { + return snapshot.IsNullSafetyEnabled(nullptr); + } + private: FML_DISALLOW_COPY_AND_ASSIGN(AppSnapshotIsolateConfiguration); }; @@ -50,6 +54,11 @@ class KernelIsolateConfiguration : public IsolateConfiguration { return isolate.PrepareForRunningFromKernel(std::move(kernel_)); } + // |IsolateConfiguration| + bool IsNullSafetyEnabled(const DartSnapshot& snapshot) override { + return snapshot.IsNullSafetyEnabled(kernel_.get()); + } + private: std::unique_ptr kernel_; @@ -61,7 +70,12 @@ class KernelListIsolateConfiguration final : public IsolateConfiguration { KernelListIsolateConfiguration( std::vector>> kernel_pieces) - : kernel_pieces_(std::move(kernel_pieces)) {} + : kernel_piece_futures_(std::move(kernel_pieces)) { + if (kernel_piece_futures_.empty()) { + FML_LOG(ERROR) << "Attempted to create kernel list configuration without " + "any kernel blobs."; + } + } // |IsolateConfiguration| bool DoPrepareIsolate(DartIsolate& isolate) override { @@ -69,11 +83,22 @@ class KernelListIsolateConfiguration final : public IsolateConfiguration { return false; } - for (size_t i = 0; i < kernel_pieces_.size(); i++) { - bool last_piece = i + 1 == kernel_pieces_.size(); + ResolveKernelPiecesIfNecessary(); - if (!isolate.PrepareForRunningFromKernel(kernel_pieces_[i].get(), - last_piece)) { + if (resolved_kernel_pieces_.empty()) { + FML_DLOG(ERROR) << "No kernel pieces provided to prepare this isolate."; + return false; + } + + for (size_t i = 0; i < resolved_kernel_pieces_.size(); i++) { + if (!resolved_kernel_pieces_[i]) { + FML_DLOG(ERROR) << "This kernel list isolate configuration was already " + "used to prepare an isolate."; + return false; + } + const bool last_piece = i + 1 == resolved_kernel_pieces_.size(); + if (!isolate.PrepareForRunningFromKernel( + std::move(resolved_kernel_pieces_[i]), last_piece)) { return false; } } @@ -81,8 +106,36 @@ class KernelListIsolateConfiguration final : public IsolateConfiguration { return true; } + // |IsolateConfiguration| + bool IsNullSafetyEnabled(const DartSnapshot& snapshot) override { + ResolveKernelPiecesIfNecessary(); + const auto kernel = resolved_kernel_pieces_.empty() + ? nullptr + : resolved_kernel_pieces_.front().get(); + return snapshot.IsNullSafetyEnabled(kernel); + } + + // This must be call as late as possible before accessing any of the kernel + // pieces. This will delay blocking on the futures for as long as possible. So + // far, only Fuchsia depends on this optimization and only on the non-AOT + // configs. + void ResolveKernelPiecesIfNecessary() { + if (resolved_kernel_pieces_.size() == kernel_piece_futures_.size()) { + return; + } + + resolved_kernel_pieces_.clear(); + for (auto& piece : kernel_piece_futures_) { + // The get() call will xfer the unique pointer out and leave an empty + // future in the original vector. + resolved_kernel_pieces_.emplace_back(piece.get()); + } + } + private: - std::vector>> kernel_pieces_; + std::vector>> + kernel_piece_futures_; + std::vector> resolved_kernel_pieces_; FML_DISALLOW_COPY_AND_ASSIGN(KernelListIsolateConfiguration); }; @@ -151,10 +204,6 @@ std::unique_ptr IsolateConfiguration::InferFromSettings( return CreateForAppSnapshot(); } - if (!asset_manager) { - return nullptr; - } - if (settings.application_kernels) { return CreateForKernelList(settings.application_kernels()); } @@ -166,7 +215,13 @@ std::unique_ptr IsolateConfiguration::InferFromSettings( return nullptr; } - // Running from kernel snapshot. + if (!asset_manager) { + FML_DLOG(ERROR) << "No asset manager specified when attempting to create " + "isolate configuration."; + return nullptr; + } + + // Running from kernel snapshot. Requires asset manager. { std::unique_ptr kernel = asset_manager->GetAsMapping(settings.application_kernel_asset); @@ -175,7 +230,14 @@ std::unique_ptr IsolateConfiguration::InferFromSettings( } } - // Running from kernel divided into several pieces (for sharing). + // Running from kernel divided into several pieces (for sharing). Requires + // asset manager and io worker. + + if (!io_worker) { + FML_DLOG(ERROR) << "No IO worker specified to load kernel pieces."; + return nullptr; + } + { std::unique_ptr kernel_list = asset_manager->GetAsMapping(settings.application_kernel_list_asset); @@ -207,6 +269,10 @@ std::unique_ptr IsolateConfiguration::CreateForKernelList( std::vector> kernel_pieces) { std::vector>> pieces; for (auto& piece : kernel_pieces) { + if (!piece) { + FML_DLOG(ERROR) << "Invalid kernel piece."; + continue; + } std::promise> promise; pieces.push_back(promise.get_future()); promise.set_value(std::move(piece)); diff --git a/shell/common/isolate_configuration.h b/runtime/isolate_configuration.h similarity index 90% rename from shell/common/isolate_configuration.h rename to runtime/isolate_configuration.h index c5f286103a0d6..1fc3efef51323 100644 --- a/shell/common/isolate_configuration.h +++ b/runtime/isolate_configuration.h @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#ifndef FLUTTER_SHELL_COMMON_ISOLATE_CONFIGURATION_H_ -#define FLUTTER_SHELL_COMMON_ISOLATE_CONFIGURATION_H_ +#ifndef FLUTTER_SHELL_RUNTIME_ISOLATE_CONFIGURATION_H_ +#define FLUTTER_SHELL_RUNTIME_ISOLATE_CONFIGURATION_H_ #include #include @@ -50,7 +50,10 @@ class IsolateConfiguration { /// for which this run configuration is used is collected. /// /// @param[in] settings The settings - /// @param[in] asset_manager The asset manager + /// @param[in] asset_manager The optional asset manager. This is used when + /// using the legacy settings fields that specify + /// the asset by name instead of a mappings + /// callback. /// @param[in] io_worker An optional IO worker. Specify `nullptr` is a /// worker should not be used or one is not /// available. @@ -58,10 +61,10 @@ class IsolateConfiguration { /// @return An isolate configuration if one can be inferred from the /// settings. If not, returns `nullptr`. /// - static std::unique_ptr InferFromSettings( + [[nodiscard]] static std::unique_ptr InferFromSettings( const Settings& settings, - std::shared_ptr asset_manager, - fml::RefPtr io_worker); + std::shared_ptr asset_manager = nullptr, + fml::RefPtr io_worker = nullptr); //---------------------------------------------------------------------------- /// @brief Creates an AOT isolate configuration using snapshot symbols @@ -152,7 +155,9 @@ class IsolateConfiguration { /// returns true, the engine will not move the isolate to the /// `DartIsolate::Phase::Ready` phase for subsequent run. /// - bool PrepareIsolate(DartIsolate& isolate); + [[nodiscard]] bool PrepareIsolate(DartIsolate& isolate); + + virtual bool IsNullSafetyEnabled(const DartSnapshot& snapshot) = 0; protected: virtual bool DoPrepareIsolate(DartIsolate& isolate) = 0; @@ -163,4 +168,4 @@ class IsolateConfiguration { } // namespace flutter -#endif // FLUTTER_SHELL_COMMON_ISOLATE_CONFIGURATION_H_ +#endif // FLUTTER_SHELL_RUNTIME_ISOLATE_CONFIGURATION_H_ diff --git a/runtime/runtime_controller.cc b/runtime/runtime_controller.cc index 7761bee4ea7ea..83d1292d3fd00 100644 --- a/runtime/runtime_controller.cc +++ b/runtime/runtime_controller.cc @@ -11,6 +11,7 @@ #include "flutter/lib/ui/window/platform_configuration.h" #include "flutter/lib/ui/window/viewport_metrics.h" #include "flutter/lib/ui/window/window.h" +#include "flutter/runtime/isolate_configuration.h" #include "flutter/runtime/runtime_delegate.h" #include "third_party/tonic/dart_message_handler.h" @@ -52,50 +53,7 @@ RuntimeController::RuntimeController( platform_data_(std::move(p_platform_data)), isolate_create_callback_(p_isolate_create_callback), isolate_shutdown_callback_(p_isolate_shutdown_callback), - persistent_isolate_data_(std::move(p_persistent_isolate_data)) { - // Create the root isolate as soon as the runtime controller is initialized. - // It will be run at a later point when the engine provides a run - // configuration and then runs the isolate. - auto strong_root_isolate = - DartIsolate::CreateRootIsolate( - vm_->GetVMData()->GetSettings(), // - isolate_snapshot_, // - task_runners_, // - std::make_unique(this), // - snapshot_delegate_, // - hint_freed_delegate_, // - io_manager_, // - unref_queue_, // - image_decoder_, // - p_advisory_script_uri, // - p_advisory_script_entrypoint, // - nullptr, // - isolate_create_callback_, // - isolate_shutdown_callback_ // - ) - .lock(); - - FML_CHECK(strong_root_isolate) << "Could not create root isolate."; - - // The root isolate ivar is weak. - root_isolate_ = strong_root_isolate; - - strong_root_isolate->SetReturnCodeCallback([this](uint32_t code) { - root_isolate_return_code_ = {true, code}; - }); - - if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { - tonic::DartState::Scope scope(strong_root_isolate); - platform_configuration->DidCreateIsolate(); - if (!FlushRuntimeStateToIsolate()) { - FML_DLOG(ERROR) << "Could not setup initial isolate state."; - } - } else { - FML_DCHECK(false) << "RuntimeController created without window binding."; - } - - FML_DCHECK(Dart_CurrentIsolate() == nullptr); -} + persistent_isolate_data_(std::move(p_persistent_isolate_data)) {} RuntimeController::~RuntimeController() { FML_DCHECK(Dart_CurrentIsolate() == nullptr); @@ -110,8 +68,8 @@ RuntimeController::~RuntimeController() { } } -bool RuntimeController::IsRootIsolateRunning() const { - std::shared_ptr root_isolate = root_isolate_.lock(); +bool RuntimeController::IsRootIsolateRunning() { + std::shared_ptr root_isolate = GetRootIsolate().lock(); if (root_isolate) { return root_isolate->GetPhase() == DartIsolate::Phase::Running; } @@ -238,7 +196,7 @@ bool RuntimeController::ReportTimings(std::vector timings) { } bool RuntimeController::NotifyIdle(int64_t deadline, size_t freed_hint) { - std::shared_ptr root_isolate = root_isolate_.lock(); + std::shared_ptr root_isolate = GetRootIsolate().lock(); if (!root_isolate) { return false; } @@ -298,7 +256,7 @@ bool RuntimeController::DispatchSemanticsAction(int32_t id, PlatformConfiguration* RuntimeController::GetPlatformConfigurationIfAvailable() { - std::shared_ptr root_isolate = root_isolate_.lock(); + std::shared_ptr root_isolate = GetRootIsolate().lock(); return root_isolate ? root_isolate->platform_configuration() : nullptr; } @@ -360,17 +318,17 @@ RuntimeController::ComputePlatformResolvedLocale( } Dart_Port RuntimeController::GetMainPort() { - std::shared_ptr root_isolate = root_isolate_.lock(); + std::shared_ptr root_isolate = GetRootIsolate().lock(); return root_isolate ? root_isolate->main_port() : ILLEGAL_PORT; } std::string RuntimeController::GetIsolateName() { - std::shared_ptr root_isolate = root_isolate_.lock(); + std::shared_ptr root_isolate = GetRootIsolate().lock(); return root_isolate ? root_isolate->debug_name() : ""; } bool RuntimeController::HasLivePorts() { - std::shared_ptr root_isolate = root_isolate_.lock(); + std::shared_ptr root_isolate = GetRootIsolate().lock(); if (!root_isolate) { return false; } @@ -379,15 +337,90 @@ bool RuntimeController::HasLivePorts() { } tonic::DartErrorHandleType RuntimeController::GetLastError() { - std::shared_ptr root_isolate = root_isolate_.lock(); + std::shared_ptr root_isolate = GetRootIsolate().lock(); return root_isolate ? root_isolate->GetLastError() : tonic::kNoError; } +bool RuntimeController::LaunchRootIsolate( + const Settings& settings, + std::optional dart_entrypoint, + std::optional dart_entrypoint_library, + std::unique_ptr isolate_configuration) { + if (root_isolate_.lock()) { + FML_LOG(ERROR) << "Root isolate was already running."; + return false; + } + + auto strong_root_isolate = + DartIsolate::CreateRunningRootIsolate( + settings, // + isolate_snapshot_, // + task_runners_, // + std::make_unique(this), // + snapshot_delegate_, // + hint_freed_delegate_, // + io_manager_, // + unref_queue_, // + image_decoder_, // + advisory_script_uri_, // + advisory_script_entrypoint_, // + DartIsolate::Flags{}, // + isolate_create_callback_, // + isolate_shutdown_callback_, // + dart_entrypoint, // + dart_entrypoint_library, // + std::move(isolate_configuration) // + ) + .lock(); + + if (!strong_root_isolate) { + FML_LOG(ERROR) << "Could not create root isolate."; + return false; + } + + // The root isolate ivar is weak. + root_isolate_ = strong_root_isolate; + + // Capture by `this` here is safe because the callback is made by the dart + // state itself. The isolate (and its Dart state) is owned by this object and + // it will be collected before this object. + strong_root_isolate->SetReturnCodeCallback( + [this](uint32_t code) { root_isolate_return_code_ = code; }); + + if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { + tonic::DartState::Scope scope(strong_root_isolate); + platform_configuration->DidCreateIsolate(); + if (!FlushRuntimeStateToIsolate()) { + FML_DLOG(ERROR) << "Could not setup initial isolate state."; + } + } else { + FML_DCHECK(false) << "RuntimeController created without window binding."; + } + + FML_DCHECK(Dart_CurrentIsolate() == nullptr); + + client_.OnRootIsolateCreated(); + + return true; +} + +std::optional RuntimeController::GetRootIsolateServiceID() const { + if (auto isolate = root_isolate_.lock()) { + return isolate->GetServiceId(); + } + return std::nullopt; +} + std::weak_ptr RuntimeController::GetRootIsolate() { + std::shared_ptr root_isolate = root_isolate_.lock(); + if (root_isolate) { + return root_isolate_; + } + return root_isolate_; } -std::pair RuntimeController::GetRootIsolateReturnCode() { +std::optional RuntimeController::GetRootIsolateReturnCode() { return root_isolate_return_code_; } diff --git a/runtime/runtime_controller.h b/runtime/runtime_controller.h index 4767d01fbcb13..17b02b935f986 100644 --- a/runtime/runtime_controller.h +++ b/runtime/runtime_controller.h @@ -5,6 +5,7 @@ #ifndef FLUTTER_RUNTIME_RUNTIME_CONTROLLER_H_ #define FLUTTER_RUNTIME_RUNTIME_CONTROLLER_H_ +#include #include #include @@ -95,7 +96,7 @@ class RuntimeController : public PlatformConfigurationClient { /// code in isolate scope when the VM /// is about to be notified that the /// engine is going to be idle. - /// @param[in] platform_data The window data (if exists). + /// @param[in] platform_data The window data (if exists). /// @param[in] isolate_create_callback The isolate create callback. This /// allows callers to run native code /// in isolate scope on the UI task @@ -132,10 +133,41 @@ class RuntimeController : public PlatformConfigurationClient { ~RuntimeController() override; //---------------------------------------------------------------------------- - /// @brief Clone the the runtime controller. This re-creates the root - /// isolate with the same snapshots and copies all window data to - /// the new instance. This is usually only used in the debug - /// runtime mode to support the cold-restart scenario. + /// @brief Launches the isolate using the window data associated with + /// this runtime controller. Before this call, the Dart isolate + /// has not been initialized. On successful return, the caller can + /// assume that the isolate is in the + /// `DartIsolate::Phase::Running` phase. + /// + /// This call will fail if a root isolate is already running. To + /// re-create an isolate with the window data associated with this + /// runtime controller, `Clone` this runtime controller and + /// Launch an isolate in that runtime controller instead. + /// + /// @param[in] settings The per engine instance settings. + /// @param[in] dart_entrypoint The dart entrypoint. If + /// `std::nullopt` or empty, `main` will + /// be attempted. + /// @param[in] dart_entrypoint_library The dart entrypoint library. If + /// `std::nullopt` or empty, the core + /// library will be attempted. + /// @param[in] isolate_configuration The isolate configuration + /// + /// @return If the isolate could be launched and guided to the + /// `DartIsolate::Phase::Running` phase. + /// + [[nodiscard]] bool LaunchRootIsolate( + const Settings& settings, + std::optional dart_entrypoint, + std::optional dart_entrypoint_library, + std::unique_ptr isolate_configuration); + + //---------------------------------------------------------------------------- + /// @brief Clone the the runtime controller. Launching an isolate with a + /// cloned runtime controller will use the same snapshots and + /// copies all window data to the new instance. This is usually + /// only used in the debug runtime mode to support the + /// cold-restart scenario. /// /// @return A clone of the existing runtime controller. /// @@ -348,7 +380,7 @@ class RuntimeController : public PlatformConfigurationClient { /// /// @return True if root isolate running, False otherwise. /// - virtual bool IsRootIsolateRunning() const; + virtual bool IsRootIsolateRunning(); //---------------------------------------------------------------------------- /// @brief Dispatch the specified platform message to running root @@ -427,26 +459,20 @@ class RuntimeController : public PlatformConfigurationClient { tonic::DartErrorHandleType GetLastError(); //---------------------------------------------------------------------------- - /// @brief Get a weak pointer to the root Dart isolate. This isolate may - /// only be locked on the UI task runner. Callers use this - /// accessor to transition to the root isolate to the running - /// phase. + /// @brief Get the service ID of the root isolate if the root isolate is + /// running. /// - /// @return The root isolate reference. + /// @return The root isolate service id. /// - std::weak_ptr GetRootIsolate(); + std::optional GetRootIsolateServiceID() const; //---------------------------------------------------------------------------- /// @brief Get the return code specified by the root isolate (if one is /// present). /// - /// @bug Change this method to return `std::optional` - /// instead. - /// - /// @return The root isolate return code. The first argument in the pair - /// indicates if one is specified by the root isolate. + /// @return The root isolate return code if the isolate has specified one. /// - std::pair GetRootIsolateReturnCode(); + std::optional GetRootIsolateReturnCode(); protected: /// Constructor for Mocks. @@ -480,8 +506,12 @@ class RuntimeController : public PlatformConfigurationClient { std::string advisory_script_entrypoint_; std::function idle_notification_callback_; PlatformData platform_data_; + std::future create_and_config_root_isolate_; + // Note that `root_isolate_` is created asynchronously from the constructor of + // `RuntimeController`, be careful to use it directly while it might have not + // been created yet. Call `GetRootIsolate()` instead which guarantees that. std::weak_ptr root_isolate_; - std::pair root_isolate_return_code_ = {false, 0}; + std::optional root_isolate_return_code_; const fml::closure isolate_create_callback_; const fml::closure isolate_shutdown_callback_; std::shared_ptr persistent_isolate_data_; @@ -522,6 +552,16 @@ class RuntimeController : public PlatformConfigurationClient { std::unique_ptr> ComputePlatformResolvedLocale( const std::vector& supported_locale_data) override; + //---------------------------------------------------------------------------- + /// @brief Get a weak pointer to the root Dart isolate. This isolate may + /// only be locked on the UI task runner. Callers use this + /// accessor to transition to the root isolate to the running + /// phase. + /// + /// @return The root isolate reference. + /// + std::weak_ptr GetRootIsolate(); + FML_DISALLOW_COPY_AND_ASSIGN(RuntimeController); }; diff --git a/runtime/runtime_delegate.h b/runtime/runtime_delegate.h index 20059827b8150..55978b4dbc39f 100644 --- a/runtime/runtime_delegate.h +++ b/runtime/runtime_delegate.h @@ -32,6 +32,8 @@ class RuntimeDelegate { virtual FontCollection& GetFontCollection() = 0; + virtual void OnRootIsolateCreated() = 0; + virtual void UpdateIsolateDescription(const std::string isolate_name, int64_t isolate_port) = 0; diff --git a/runtime/type_conversions_unittests.cc b/runtime/type_conversions_unittests.cc new file mode 100644 index 0000000000000..f1ed831e174cc --- /dev/null +++ b/runtime/type_conversions_unittests.cc @@ -0,0 +1,174 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/runtime/dart_vm_lifecycle.h" +#include "flutter/testing/dart_isolate_runner.h" +#include "flutter/testing/fixture_test.h" +#include "flutter/testing/testing.h" +#include "flutter/third_party/tonic/converter/dart_converter.h" + +namespace flutter { +namespace testing { + +class TypeConversionsTest : public FixtureTest { + public: + TypeConversionsTest() + : settings_(CreateSettingsForFixture()), + vm_(DartVMRef::Create(settings_)) {} + + ~TypeConversionsTest() = default; + + [[nodiscard]] bool RunWithEntrypoint(const std::string& entrypoint) { + if (running_isolate_) { + return false; + } + auto thread = CreateNewThread(); + TaskRunners single_threaded_task_runner(GetCurrentTestName(), thread, + thread, thread, thread); + auto isolate = + RunDartCodeInIsolate(vm_, settings_, single_threaded_task_runner, + entrypoint, {}, GetFixturesPath()); + if (!isolate || isolate->get()->GetPhase() != DartIsolate::Phase::Running) { + return false; + } + + running_isolate_ = std::move(isolate); + return true; + } + + private: + Settings settings_; + DartVMRef vm_; + std::unique_ptr running_isolate_; + FML_DISALLOW_COPY_AND_ASSIGN(TypeConversionsTest); +}; + +TEST_F(TypeConversionsTest, TestFixture) { + ASSERT_TRUE(RunWithEntrypoint("main")); +} + +TEST_F(TypeConversionsTest, CanConvertEmptyList) { + fml::AutoResetWaitableEvent event; + AddNativeCallback( + "NotifySuccess", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + auto bool_handle = Dart_GetNativeArgument(args, 0); + ASSERT_FALSE(tonic::LogIfError(bool_handle)); + ASSERT_TRUE(tonic::DartConverter::FromDart(bool_handle)); + event.Signal(); + })); + AddNativeCallback( + "NotifyNative", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments) { + std::vector items; + auto items_handle = tonic::ToDart(items); + ASSERT_FALSE(tonic::LogIfError(items_handle)); + tonic::DartInvokeField(::Dart_RootLibrary(), "testCanConvertEmptyList", + {items_handle}); + })); + ASSERT_TRUE(RunWithEntrypoint("trampoline")); + event.Wait(); +} + +TEST_F(TypeConversionsTest, CanConvertListOfStrings) { + fml::AutoResetWaitableEvent event; + AddNativeCallback( + "NotifySuccess", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + auto bool_handle = Dart_GetNativeArgument(args, 0); + ASSERT_FALSE(tonic::LogIfError(bool_handle)); + ASSERT_TRUE(tonic::DartConverter::FromDart(bool_handle)); + event.Signal(); + })); + AddNativeCallback( + "NotifyNative", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments) { + std::vector items; + items.push_back("tinker"); + items.push_back("tailor"); + items.push_back("soldier"); + items.push_back("sailor"); + auto items_handle = tonic::ToDart(items); + ASSERT_FALSE(tonic::LogIfError(items_handle)); + tonic::DartInvokeField(::Dart_RootLibrary(), + "testCanConvertListOfStrings", {items_handle}); + })); + ASSERT_TRUE(RunWithEntrypoint("trampoline")); + event.Wait(); +} + +TEST_F(TypeConversionsTest, CanConvertListOfDoubles) { + fml::AutoResetWaitableEvent event; + AddNativeCallback( + "NotifySuccess", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + auto bool_handle = Dart_GetNativeArgument(args, 0); + ASSERT_FALSE(tonic::LogIfError(bool_handle)); + ASSERT_TRUE(tonic::DartConverter::FromDart(bool_handle)); + event.Signal(); + })); + AddNativeCallback( + "NotifyNative", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments) { + std::vector items; + items.push_back(1.0); + items.push_back(2.0); + items.push_back(3.0); + items.push_back(4.0); + auto items_handle = tonic::ToDart(items); + ASSERT_FALSE(tonic::LogIfError(items_handle)); + tonic::DartInvokeField(::Dart_RootLibrary(), + "testCanConvertListOfDoubles", {items_handle}); + })); + ASSERT_TRUE(RunWithEntrypoint("trampoline")); + event.Wait(); +} + +TEST_F(TypeConversionsTest, CanConvertListOfInts) { + fml::AutoResetWaitableEvent event; + AddNativeCallback( + "NotifySuccess", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + auto bool_handle = Dart_GetNativeArgument(args, 0); + ASSERT_FALSE(tonic::LogIfError(bool_handle)); + ASSERT_TRUE(tonic::DartConverter::FromDart(bool_handle)); + event.Signal(); + })); + AddNativeCallback( + "NotifyNative", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments) { + std::vector items; + items.push_back(1); + items.push_back(2); + items.push_back(3); + items.push_back(4); + auto items_handle = tonic::ToDart(items); + ASSERT_FALSE(tonic::LogIfError(items_handle)); + tonic::DartInvokeField(::Dart_RootLibrary(), "testCanConvertListOfInts", + {items_handle}); + })); + ASSERT_TRUE(RunWithEntrypoint("trampoline")); + event.Wait(); +} + +TEST_F(TypeConversionsTest, CanConvertListOfFloatsToListOfDartDoubles) { + fml::AutoResetWaitableEvent event; + AddNativeCallback( + "NotifySuccess", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + auto bool_handle = Dart_GetNativeArgument(args, 0); + ASSERT_FALSE(tonic::LogIfError(bool_handle)); + ASSERT_TRUE(tonic::DartConverter::FromDart(bool_handle)); + event.Signal(); + })); + AddNativeCallback( + "NotifyNative", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments) { + std::vector items; + items.push_back(1.0f); + items.push_back(2.0f); + items.push_back(3.0f); + items.push_back(4.0f); + auto items_handle = tonic::ToDart(items); + ASSERT_FALSE(tonic::LogIfError(items_handle)); + // This will fail on type mismatch. + tonic::DartInvokeField(::Dart_RootLibrary(), + "testCanConvertListOfDoubles", {items_handle}); + })); + ASSERT_TRUE(RunWithEntrypoint("trampoline")); + event.Wait(); +} + +} // namespace testing +} // namespace flutter diff --git a/shell/common/BUILD.gn b/shell/common/BUILD.gn index eff5742eebee8..c988e88c2a1a6 100644 --- a/shell/common/BUILD.gn +++ b/shell/common/BUILD.gn @@ -69,8 +69,6 @@ source_set("common") { "display_manager.h", "engine.cc", "engine.h", - "isolate_configuration.cc", - "isolate_configuration.h", "persistent_cache.cc", "persistent_cache.h", "pipeline.cc", diff --git a/shell/common/animator_unittests.cc b/shell/common/animator_unittests.cc index 0c28908af407b..281f204fb869a 100644 --- a/shell/common/animator_unittests.cc +++ b/shell/common/animator_unittests.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #define FML_USED_ON_EMBEDDER diff --git a/shell/common/engine.cc b/shell/common/engine.cc index d2384c03579cf..6c9304f1ad945 100644 --- a/shell/common/engine.cc +++ b/shell/common/engine.cc @@ -106,6 +106,10 @@ void Engine::SetupDefaultFontManager() { font_collection_.SetupDefaultFontManager(); } +std::shared_ptr Engine::GetAssetManager() { + return asset_manager_; +} + bool Engine::UpdateAssetManager( std::shared_ptr new_asset_manager) { if (asset_manager_ == new_asset_manager) { @@ -149,90 +153,33 @@ Engine::RunStatus Engine::Run(RunConfiguration configuration) { last_entry_point_ = configuration.GetEntrypoint(); last_entry_point_library_ = configuration.GetEntrypointLibrary(); - auto isolate_launch_status = - PrepareAndLaunchIsolate(std::move(configuration)); - if (isolate_launch_status == Engine::RunStatus::Failure) { - FML_LOG(ERROR) << "Engine not prepare and launch isolate."; - return isolate_launch_status; - } else if (isolate_launch_status == - Engine::RunStatus::FailureAlreadyRunning) { - return isolate_launch_status; - } - - std::shared_ptr isolate = - runtime_controller_->GetRootIsolate().lock(); - - bool isolate_running = - isolate && isolate->GetPhase() == DartIsolate::Phase::Running; - - if (isolate_running) { - tonic::DartState::Scope scope(isolate.get()); - - if (settings_.root_isolate_create_callback) { - settings_.root_isolate_create_callback(); - } - - if (settings_.root_isolate_shutdown_callback) { - isolate->AddIsolateShutdownCallback( - settings_.root_isolate_shutdown_callback); - } - - std::string service_id = isolate->GetServiceId(); - fml::RefPtr service_id_message = - fml::MakeRefCounted( - kIsolateChannel, - std::vector(service_id.begin(), service_id.end()), - nullptr); - HandlePlatformMessage(service_id_message); - } - - return isolate_running ? Engine::RunStatus::Success - : Engine::RunStatus::Failure; -} - -Engine::RunStatus Engine::PrepareAndLaunchIsolate( - RunConfiguration configuration) { - TRACE_EVENT0("flutter", "Engine::PrepareAndLaunchIsolate"); - UpdateAssetManager(configuration.GetAssetManager()); - auto isolate_configuration = configuration.TakeIsolateConfiguration(); - - std::shared_ptr isolate = - runtime_controller_->GetRootIsolate().lock(); - - if (!isolate) { - return RunStatus::Failure; - } - - // This can happen on iOS after a plugin shows a native window and returns to - // the Flutter ViewController. - if (isolate->GetPhase() == DartIsolate::Phase::Running) { - FML_DLOG(WARNING) << "Isolate was already running!"; + if (runtime_controller_->IsRootIsolateRunning()) { return RunStatus::FailureAlreadyRunning; } - if (!isolate_configuration->PrepareIsolate(*isolate)) { - FML_LOG(ERROR) << "Could not prepare to run the isolate."; + if (!runtime_controller_->LaunchRootIsolate( + settings_, // + configuration.GetEntrypoint(), // + configuration.GetEntrypointLibrary(), // + configuration.TakeIsolateConfiguration()) // + ) { return RunStatus::Failure; } - if (configuration.GetEntrypointLibrary().empty()) { - if (!isolate->Run(configuration.GetEntrypoint(), - settings_.dart_entrypoint_args)) { - FML_LOG(ERROR) << "Could not run the isolate."; - return RunStatus::Failure; - } - } else { - if (!isolate->RunFromLibrary(configuration.GetEntrypointLibrary(), - configuration.GetEntrypoint(), - settings_.dart_entrypoint_args)) { - FML_LOG(ERROR) << "Could not run the isolate."; - return RunStatus::Failure; - } + auto service_id = runtime_controller_->GetRootIsolateServiceID(); + if (service_id.has_value()) { + fml::RefPtr service_id_message = + fml::MakeRefCounted( + kIsolateChannel, + std::vector(service_id.value().begin(), + service_id.value().end()), + nullptr); + HandlePlatformMessage(service_id_message); } - return RunStatus::Success; + return Engine::RunStatus::Success; } void Engine::BeginFrame(fml::TimePoint frame_time) { @@ -257,7 +204,7 @@ void Engine::NotifyIdle(int64_t deadline) { hint_freed_bytes_since_last_idle_ = 0; } -std::pair Engine::GetUIIsolateReturnCode() { +std::optional Engine::GetUIIsolateReturnCode() { return runtime_controller_->GetRootIsolateReturnCode(); } @@ -497,6 +444,10 @@ void Engine::HandlePlatformMessage(fml::RefPtr message) { } } +void Engine::OnRootIsolateCreated() { + delegate_.OnRootIsolateCreated(); +} + void Engine::UpdateIsolateDescription(const std::string isolate_name, int64_t isolate_port) { delegate_.UpdateIsolateDescription(isolate_name, isolate_port); diff --git a/shell/common/engine.h b/shell/common/engine.h index b8d98aa766291..3319e9c9e28c5 100644 --- a/shell/common/engine.h +++ b/shell/common/engine.h @@ -189,6 +189,15 @@ class Engine final : public RuntimeDelegate, /// virtual void OnPreEngineRestart() = 0; + //-------------------------------------------------------------------------- + /// @brief Notifies the shell that the root isolate is created. + /// Currently, this information is to add to the service + /// protocol list of available root isolates running in the VM + /// and their names so that the appropriate isolate can be + /// selected in the tools for debugging and instrumentation. + /// + virtual void OnRootIsolateCreated() = 0; + //-------------------------------------------------------------------------- /// @brief Notifies the shell of the name of the root isolate and its /// port when that isolate is launched, restarted (in the @@ -599,14 +608,9 @@ class Engine final : public RuntimeDelegate, /// /// @see `UIIsolateHasLivePorts` /// - // TODO(chinmaygarde): Use std::optional instead of the pair now that it is - // available. + /// @return The return code (if specified) by the isolate. /// - /// @return A pair containing a boolean value indicating if the isolate - /// set a "return value" and that value if present. When the first - /// item of the pair is false, second item is meaningless. - /// - std::pair GetUIIsolateReturnCode(); + std::optional GetUIIsolateReturnCode(); //---------------------------------------------------------------------------- /// @brief Indicates to the Flutter application that it has obtained a @@ -734,6 +738,9 @@ class Engine final : public RuntimeDelegate, // |RuntimeDelegate| FontCollection& GetFontCollection() override; + // Return the asset manager associated with the current engine, or nullptr. + std::shared_ptr GetAssetManager(); + // |PointerDataDispatcher::Delegate| void DoDispatchPacket(std::unique_ptr packet, uint64_t trace_flow_id) override; @@ -796,6 +803,9 @@ class Engine final : public RuntimeDelegate, // |RuntimeDelegate| void HandlePlatformMessage(fml::RefPtr message) override; + // |RuntimeDelegate| + void OnRootIsolateCreated() override; + // |RuntimeDelegate| void UpdateIsolateDescription(const std::string isolate_name, int64_t isolate_port) override; @@ -822,8 +832,6 @@ class Engine final : public RuntimeDelegate, bool GetAssetAsBuffer(const std::string& name, std::vector* data); - RunStatus PrepareAndLaunchIsolate(RunConfiguration configuration); - friend class testing::ShellTest; FML_DISALLOW_COPY_AND_ASSIGN(Engine); diff --git a/shell/common/engine_unittests.cc b/shell/common/engine_unittests.cc index 3390507f68d9b..7405511513a9b 100644 --- a/shell/common/engine_unittests.cc +++ b/shell/common/engine_unittests.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #include "flutter/shell/common/engine.h" @@ -27,6 +26,7 @@ class MockDelegate : public Engine::Delegate { MOCK_METHOD1(OnEngineHandlePlatformMessage, void(fml::RefPtr)); MOCK_METHOD0(OnPreEngineRestart, void()); + MOCK_METHOD0(OnRootIsolateCreated, void()); MOCK_METHOD2(UpdateIsolateDescription, void(const std::string, int64_t)); MOCK_METHOD1(SetNeedsReportTimings, void(bool)); MOCK_METHOD1(ComputePlatformResolvedLocale, @@ -49,6 +49,7 @@ class MockRuntimeDelegate : public RuntimeDelegate { void(SemanticsNodeUpdates, CustomAccessibilityActionUpdates)); MOCK_METHOD1(HandlePlatformMessage, void(fml::RefPtr)); MOCK_METHOD0(GetFontCollection, FontCollection&()); + MOCK_METHOD0(OnRootIsolateCreated, void()); MOCK_METHOD2(UpdateIsolateDescription, void(const std::string, int64_t)); MOCK_METHOD1(SetNeedsReportTimings, void(bool)); MOCK_METHOD1(ComputePlatformResolvedLocale, @@ -60,7 +61,7 @@ class MockRuntimeController : public RuntimeController { public: MockRuntimeController(RuntimeDelegate& client, TaskRunners p_task_runners) : RuntimeController(client, p_task_runners) {} - MOCK_CONST_METHOD0(IsRootIsolateRunning, bool()); + MOCK_METHOD0(IsRootIsolateRunning, bool()); MOCK_METHOD1(DispatchPlatformMessage, bool(fml::RefPtr)); }; diff --git a/shell/common/fixtures/shell_test.dart b/shell/common/fixtures/shell_test.dart index 492ddb8960d03..15d9386df12e7 100644 --- a/shell/common/fixtures/shell_test.dart +++ b/shell/common/fixtures/shell_test.dart @@ -87,7 +87,7 @@ void testCanLaunchSecondaryIsolate() { @pragma('vm:entry-point') void testSkiaResourceCacheSendsResponse() { - final PlatformMessageResponseCallback callback = (ByteData data) { + final PlatformMessageResponseCallback callback = (ByteData? data) { if (data == null) { throw 'Response must not be null.'; } @@ -130,7 +130,7 @@ void canCreateImageFromDecompressedData() { @pragma('vm:entry-point') void canAccessIsolateLaunchData() { - notifyMessage(utf8.decode(window.getPersistentIsolateData().buffer.asUint8List())); + notifyMessage(utf8.decode(window.getPersistentIsolateData()!.buffer.asUint8List())); } void notifyMessage(String string) native 'NotifyMessage'; @@ -174,7 +174,7 @@ void canAccessResourceFromAssetDir() async { window.sendPlatformMessage( 'flutter/assets', Uint8List.fromList(utf8.encode('kernel_blob.bin')).buffer.asByteData(), - (ByteData byteData) { + (ByteData? byteData) { notifyCanAccessResource(byteData != null); }, ); diff --git a/shell/common/input_events_unittests.cc b/shell/common/input_events_unittests.cc index dfb40d8459f4d..f7514beabc4b5 100644 --- a/shell/common/input_events_unittests.cc +++ b/shell/common/input_events_unittests.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #include "flutter/shell/common/shell_test.h" #include "flutter/testing/testing.h" diff --git a/shell/common/persistent_cache_unittests.cc b/shell/common/persistent_cache_unittests.cc index a9ecfb1b39adb..77c5a8a9ddf52 100644 --- a/shell/common/persistent_cache_unittests.cc +++ b/shell/common/persistent_cache_unittests.cc @@ -185,9 +185,10 @@ TEST_F(ShellTest, CanLoadSkSLsFromAsset) { ResetAssetManager(); auto asset_manager = std::make_shared(); RunConfiguration config(nullptr, asset_manager); - asset_manager->PushBack( - std::make_unique(fml::OpenDirectory( - asset_dir.path().c_str(), false, fml::FilePermission::kRead))); + asset_manager->PushBack(std::make_unique( + fml::OpenDirectory(asset_dir.path().c_str(), false, + fml::FilePermission::kRead), + false)); CheckTwoSkSLsAreLoaded(); // 3rd, test the content of the SkSLs in the asset. diff --git a/shell/common/platform_view.cc b/shell/common/platform_view.cc index ef9f48df54ad1..a933bd3b3c6bf 100644 --- a/shell/common/platform_view.cc +++ b/shell/common/platform_view.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #include "flutter/shell/common/platform_view.h" diff --git a/shell/common/rasterizer.cc b/shell/common/rasterizer.cc index ab8f0d1fdad51..3de506ea4f5f4 100644 --- a/shell/common/rasterizer.cc +++ b/shell/common/rasterizer.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #include "flutter/shell/common/rasterizer.h" diff --git a/shell/common/run_configuration.cc b/shell/common/run_configuration.cc index 8f0966b6bc896..1b32d5c295fbd 100644 --- a/shell/common/run_configuration.cc +++ b/shell/common/run_configuration.cc @@ -21,12 +21,13 @@ RunConfiguration RunConfiguration::InferFromSettings( if (fml::UniqueFD::traits_type::IsValid(settings.assets_dir)) { asset_manager->PushBack(std::make_unique( - fml::Duplicate(settings.assets_dir))); + fml::Duplicate(settings.assets_dir), true)); } - asset_manager->PushBack( - std::make_unique(fml::OpenDirectory( - settings.assets_path.c_str(), false, fml::FilePermission::kRead))); + asset_manager->PushBack(std::make_unique( + fml::OpenDirectory(settings.assets_path.c_str(), false, + fml::FilePermission::kRead), + true)); return {IsolateConfiguration::InferFromSettings(settings, asset_manager, io_worker), diff --git a/shell/common/run_configuration.h b/shell/common/run_configuration.h index 45dfed1c2d232..cb5a5b531d248 100644 --- a/shell/common/run_configuration.h +++ b/shell/common/run_configuration.h @@ -14,7 +14,7 @@ #include "flutter/fml/macros.h" #include "flutter/fml/mapping.h" #include "flutter/fml/unique_fd.h" -#include "flutter/shell/common/isolate_configuration.h" +#include "flutter/runtime/isolate_configuration.h" namespace flutter { diff --git a/shell/common/shell.cc b/shell/common/shell.cc index 2da9e9786e385..0f470cff37c6f 100644 --- a/shell/common/shell.cc +++ b/shell/common/shell.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #define RAPIDJSON_HAS_STDSTRING 1 #include "flutter/shell/common/shell.h" @@ -564,8 +563,6 @@ bool Shell::Setup(std::unique_ptr platform_view, is_setup_ = true; - vm_->GetServiceProtocol()->AddHandler(this, GetServiceProtocolDescription()); - PersistentCache::GetCacheForProcess()->AddWorkerTaskRunner( task_runners_.GetIOTaskRunner()); @@ -1150,6 +1147,23 @@ void Shell::OnPreEngineRestart() { latch.Wait(); } +// |Engine::Delegate| +void Shell::OnRootIsolateCreated() { + if (is_added_to_service_protocol_) { + return; + } + auto description = GetServiceProtocolDescription(); + fml::TaskRunner::RunNowOrPostTask( + task_runners_.GetPlatformTaskRunner(), + [self = weak_factory_.GetWeakPtr(), + description = std::move(description)]() { + if (self) { + self->vm_->GetServiceProtocol()->AddHandler(self.get(), description); + } + }); + is_added_to_service_protocol_ = true; +} + // |Engine::Delegate| void Shell::UpdateIsolateDescription(const std::string isolate_name, int64_t isolate_port) { @@ -1295,9 +1309,15 @@ bool Shell::HandleServiceProtocolMessage( // |ServiceProtocol::Handler| ServiceProtocol::Handler::Description Shell::GetServiceProtocolDescription() const { + FML_DCHECK(task_runners_.GetUITaskRunner()->RunsTasksOnCurrentThread()); + + if (!weak_engine_) { + return ServiceProtocol::Handler::Description(); + } + return { - engine_->GetUIIsolateMainPort(), - engine_->GetUIIsolateName(), + weak_engine_->GetUIIsolateMainPort(), + weak_engine_->GetUIIsolateName(), }; } @@ -1401,10 +1421,21 @@ bool Shell::OnServiceProtocolRunInView( configuration.SetEntrypointAndLibrary(engine_->GetLastEntrypoint(), engine_->GetLastEntrypointLibrary()); - configuration.AddAssetResolver( - std::make_unique(fml::OpenDirectory( - asset_directory_path.c_str(), false, fml::FilePermission::kRead))); - configuration.AddAssetResolver(RestoreOriginalAssetResolver()); + configuration.AddAssetResolver(std::make_unique( + fml::OpenDirectory(asset_directory_path.c_str(), false, + fml::FilePermission::kRead), + false)); + + // Preserve any original asset resolvers to avoid syncing unchanged assets + // over the DevFS connection. + auto old_asset_manager = engine_->GetAssetManager(); + if (old_asset_manager != nullptr) { + for (auto& old_resolver : old_asset_manager->TakeResolvers()) { + if (old_resolver->IsValidAfterAssetManagerChange()) { + configuration.AddAssetResolver(std::move(old_resolver)); + } + } + } auto& allocator = response->GetAllocator(); response->SetObject(); @@ -1517,10 +1548,21 @@ bool Shell::OnServiceProtocolSetAssetBundlePath( auto asset_manager = std::make_shared(); - asset_manager->PushFront(RestoreOriginalAssetResolver()); asset_manager->PushFront(std::make_unique( fml::OpenDirectory(params.at("assetDirectory").data(), false, - fml::FilePermission::kRead))); + fml::FilePermission::kRead), + false)); + + // Preserve any original asset resolvers to avoid syncing unchanged assets + // over the DevFS connection. + auto old_asset_manager = engine_->GetAssetManager(); + if (old_asset_manager != nullptr) { + for (auto& old_resolver : old_asset_manager->TakeResolvers()) { + if (old_resolver->IsValidAfterAssetManagerChange()) { + asset_manager->PushBack(std::move(old_resolver)); + } + } + } if (engine_->UpdateAssetManager(std::move(asset_manager))) { response->AddMember("type", "Success", allocator); @@ -1624,16 +1666,4 @@ void Shell::OnDisplayUpdates(DisplayUpdateType update_type, display_manager_->HandleDisplayUpdates(update_type, displays); } -// Add the original asset directory to the resolvers so that unmodified assets -// bundled with the application specific format (APK, IPA) can be used without -// syncing to the Dart devFS. -std::unique_ptr Shell::RestoreOriginalAssetResolver() { - if (fml::UniqueFD::traits_type::IsValid(settings_.assets_dir)) { - return std::make_unique( - fml::Duplicate(settings_.assets_dir)); - } - return std::make_unique(fml::OpenDirectory( - settings_.assets_path.c_str(), false, fml::FilePermission::kRead)); -}; - } // namespace flutter diff --git a/shell/common/shell.h b/shell/common/shell.h index a345b0461e5fc..8e8fd18b48b3b 100644 --- a/shell/common/shell.h +++ b/shell/common/shell.h @@ -411,6 +411,7 @@ class Shell final : public PlatformView::Delegate, > service_protocol_handlers_; bool is_setup_ = false; + bool is_added_to_service_protocol_ = false; uint64_t next_pointer_flow_id_ = 0; bool first_frame_rasterized_ = false; @@ -537,6 +538,9 @@ class Shell final : public PlatformView::Delegate, // |Engine::Delegate| void OnPreEngineRestart() override; + // |Engine::Delegate| + void OnRootIsolateCreated() override; + // |Engine::Delegate| void UpdateIsolateDescription(const std::string isolate_name, int64_t isolate_port) override; diff --git a/shell/common/shell_benchmarks.cc b/shell/common/shell_benchmarks.cc index ebf2093ea01b5..012e8bdfd8ec2 100644 --- a/shell/common/shell_benchmarks.cc +++ b/shell/common/shell_benchmarks.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #include "flutter/shell/common/shell.h" diff --git a/shell/common/shell_unittests.cc b/shell/common/shell_unittests.cc index fbf0647eb1191..e15182e8d264c 100644 --- a/shell/common/shell_unittests.cc +++ b/shell/common/shell_unittests.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #define FML_USED_ON_EMBEDDER @@ -317,6 +316,8 @@ TEST_F(ShellTest, AllowedDartVMFlag) { std::vector flags = { "--enable-isolate-groups", "--no-enable-isolate-groups", + "--lazy_async_stacks", + "--no-causal_async_stacks", }; #if !FLUTTER_RELEASE flags.push_back("--max_profile_depth 1"); @@ -2184,5 +2185,28 @@ TEST_F(ShellTest, OnServiceProtocolSetAssetBundlePathWorks) { DestroyShell(std::move(shell)); } +TEST_F(ShellTest, EngineRootIsolateLaunchesDontTakeVMDataSettings) { + ASSERT_FALSE(DartVMRef::IsInstanceRunning()); + // Make sure the shell launch does not kick off the creation of the VM + // instance by already creating one upfront. + auto vm_settings = CreateSettingsForFixture(); + auto vm_ref = DartVMRef::Create(vm_settings); + ASSERT_TRUE(DartVMRef::IsInstanceRunning()); + + auto settings = vm_settings; + fml::AutoResetWaitableEvent isolate_create_latch; + settings.root_isolate_create_callback = [&](const auto& isolate) { + isolate_create_latch.Signal(); + }; + auto shell = CreateShell(settings); + ASSERT_TRUE(ValidateShell(shell.get())); + auto configuration = RunConfiguration::InferFromSettings(settings); + ASSERT_TRUE(configuration.IsValid()); + RunEngine(shell.get(), std::move(configuration)); + ASSERT_TRUE(DartVMRef::IsInstanceRunning()); + DestroyShell(std::move(shell)); + isolate_create_latch.Wait(); +} + } // namespace testing } // namespace flutter diff --git a/shell/common/skia_event_tracer_impl.cc b/shell/common/skia_event_tracer_impl.cc index c7e3392291575..053df0993c33b 100644 --- a/shell/common/skia_event_tracer_impl.cc +++ b/shell/common/skia_event_tracer_impl.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #include "flutter/shell/common/skia_event_tracer_impl.h" diff --git a/shell/common/switches.cc b/shell/common/switches.cc index 1b2398b942ff0..553e77361c6bc 100644 --- a/shell/common/switches.cc +++ b/shell/common/switches.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #include #include @@ -221,6 +220,10 @@ Settings SettingsFromCommandLine(const fml::CommandLine& command_line) { settings.enable_observatory = !command_line.HasOption(FlagForSwitch(Switch::DisableObservatory)); + // Enable mDNS Observatory Publication + settings.enable_observatory_publication = !command_line.HasOption( + FlagForSwitch(Switch::DisableObservatoryPublication)); + // Set Observatory Host if (command_line.HasOption(FlagForSwitch(Switch::DeviceObservatoryHost))) { command_line.GetOptionValue(FlagForSwitch(Switch::DeviceObservatoryHost), diff --git a/shell/common/switches.h b/shell/common/switches.h index f33e2722403df..9a9dea9401d8f 100644 --- a/shell/common/switches.h +++ b/shell/common/switches.h @@ -79,6 +79,9 @@ DEF_SWITCH(DisableObservatory, "disable-observatory", "Disable the Dart Observatory. The observatory is never available " "in release mode.") +DEF_SWITCH(DisableObservatoryPublication, + "disable-observatory-publication", + "Disable mDNS Dart Observatory publication.") DEF_SWITCH(IPv6, "ipv6", "Bind to the IPv6 localhost address for the Dart Observatory. " @@ -97,7 +100,7 @@ DEF_SWITCH(EnableSoftwareRendering, "enable-software-rendering", "Enable rendering using the Skia software backend. This is useful " "when testing Flutter on emulators. By default, Flutter will " - "attempt to either use OpenGL or Vulkan.") + "attempt to either use OpenGL, Metal, or Vulkan.") DEF_SWITCH(SkiaDeterministicRendering, "skia-deterministic-rendering", "Skips the call to SkGraphics::Init(), thus avoiding swapping out " diff --git a/shell/common/vsync_waiter_fallback.cc b/shell/common/vsync_waiter_fallback.cc index ef5cb69390cfe..fc23c6180a4ff 100644 --- a/shell/common/vsync_waiter_fallback.cc +++ b/shell/common/vsync_waiter_fallback.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #include "flutter/shell/common/vsync_waiter_fallback.h" diff --git a/shell/gpu/gpu_surface_gl.cc b/shell/gpu/gpu_surface_gl.cc index 60d02b4a0b376..a539c98570672 100644 --- a/shell/gpu/gpu_surface_gl.cc +++ b/shell/gpu/gpu_surface_gl.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #include "flutter/shell/gpu/gpu_surface_gl.h" diff --git a/shell/gpu/gpu_surface_metal.h b/shell/gpu/gpu_surface_metal.h index 42d9851d534c6..5a91307af4da7 100644 --- a/shell/gpu/gpu_surface_metal.h +++ b/shell/gpu/gpu_surface_metal.h @@ -12,12 +12,13 @@ #include "flutter/fml/platform/darwin/scoped_nsobject.h" #include "flutter/shell/gpu/gpu_surface_delegate.h" #include "third_party/skia/include/gpu/GrDirectContext.h" +#include "third_party/skia/include/gpu/mtl/GrMtlTypes.h" @class CAMetalLayer; namespace flutter { -class GPUSurfaceMetal : public Surface { +class SK_API_AVAILABLE_CA_METAL_LAYER GPUSurfaceMetal : public Surface { public: GPUSurfaceMetal(GPUSurfaceDelegate* delegate, fml::scoped_nsobject layer, diff --git a/shell/gpu/gpu_surface_metal.mm b/shell/gpu/gpu_surface_metal.mm index c6383f99ee846..2fc8a2a02dfaf 100644 --- a/shell/gpu/gpu_surface_metal.mm +++ b/shell/gpu/gpu_surface_metal.mm @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #include "flutter/shell/gpu/gpu_surface_metal.h" diff --git a/shell/gpu/gpu_surface_vulkan.cc b/shell/gpu/gpu_surface_vulkan.cc index a3982e32a4002..fe45ad5ecfab5 100644 --- a/shell/gpu/gpu_surface_vulkan.cc +++ b/shell/gpu/gpu_surface_vulkan.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #include "flutter/shell/gpu/gpu_surface_vulkan.h" diff --git a/shell/gpu/gpu_surface_vulkan_delegate.cc b/shell/gpu/gpu_surface_vulkan_delegate.cc index fa2056f8b2cdd..8b336faec2ff5 100644 --- a/shell/gpu/gpu_surface_vulkan_delegate.cc +++ b/shell/gpu/gpu_surface_vulkan_delegate.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #include "flutter/shell/gpu/gpu_surface_vulkan_delegate.h" diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 64a5e9b0cced1..91d2a368b1057 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -182,6 +182,7 @@ android_java_sources = [ "io/flutter/embedding/engine/renderer/FlutterRenderer.java", "io/flutter/embedding/engine/renderer/FlutterUiDisplayListener.java", "io/flutter/embedding/engine/renderer/RenderSurface.java", + "io/flutter/embedding/engine/renderer/SurfaceTextureWrapper.java", "io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java", "io/flutter/embedding/engine/systemchannels/KeyEventChannel.java", "io/flutter/embedding/engine/systemchannels/LifecycleChannel.java", @@ -251,6 +252,23 @@ embedding_dependencies_jars = ], "list lines") +action("check_imports") { + script = "//flutter/tools/android_illegal_imports.py" + + sources = android_java_sources + + stamp_file = "$root_out_dir/check_android_imports" + + # File does not actually get created, but GN expects us to have an output here. + outputs = [ stamp_file ] + + args = [ + "--stamp", + rebase_path(stamp_file), + "--files", + ] + rebase_path(android_java_sources) +} + action("flutter_shell_java") { script = "//build/android/gyp/javac.py" depfile = "$target_gen_dir/$target_name.d" @@ -299,7 +317,10 @@ action("flutter_shell_java") { args += rebase_path(sources, root_build_dir) - deps = [ ":gen_android_build_config_java" ] + deps = [ + ":check_imports", + ":gen_android_build_config_java", + ] } action("icudtl_object") { @@ -438,6 +459,7 @@ action("robolectric_tests") { "test/io/flutter/embedding/engine/PluginComponentTest.java", "test/io/flutter/embedding/engine/RenderingComponentTest.java", "test/io/flutter/embedding/engine/dart/DartExecutorTest.java", + "test/io/flutter/embedding/engine/dart/DartMessengerTest.java", "test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java", "test/io/flutter/embedding/engine/loader/FlutterLoaderTest.java", "test/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorViewTest.java", diff --git a/shell/platform/android/android_surface_gl.cc b/shell/platform/android/android_surface_gl.cc index c9a9a130c59c4..d7a630c345099 100644 --- a/shell/platform/android/android_surface_gl.cc +++ b/shell/platform/android/android_surface_gl.cc @@ -22,11 +22,8 @@ constexpr char kEmulatorRendererPrefix[] = AndroidSurfaceGL::AndroidSurfaceGL( std::shared_ptr android_context, std::shared_ptr jni_facade, - const AndroidSurface::Factory& surface_factory) - : external_view_embedder_( - std::make_unique(android_context, - jni_facade, - surface_factory)), + std::shared_ptr external_view_embedder) + : external_view_embedder_(external_view_embedder), android_context_( std::static_pointer_cast(android_context)), native_window_(nullptr), diff --git a/shell/platform/android/android_surface_gl.h b/shell/platform/android/android_surface_gl.h index 771fbfea41a80..ca9aead26b4c5 100644 --- a/shell/platform/android/android_surface_gl.h +++ b/shell/platform/android/android_surface_gl.h @@ -22,9 +22,10 @@ namespace flutter { class AndroidSurfaceGL final : public GPUSurfaceGLDelegate, public AndroidSurface { public: - AndroidSurfaceGL(std::shared_ptr android_context, - std::shared_ptr jni_facade, - const AndroidSurface::Factory& surface_factory); + AndroidSurfaceGL( + std::shared_ptr android_context, + std::shared_ptr jni_facade, + std::shared_ptr external_view_embedder); ~AndroidSurfaceGL() override; @@ -69,7 +70,7 @@ class AndroidSurfaceGL final : public GPUSurfaceGLDelegate, sk_sp GetGLInterface() const override; private: - const std::unique_ptr external_view_embedder_; + const std::shared_ptr external_view_embedder_; const std::shared_ptr android_context_; fml::RefPtr native_window_; diff --git a/shell/platform/android/android_surface_software.cc b/shell/platform/android/android_surface_software.cc index 126f127bd2b8c..ab116f79048fa 100644 --- a/shell/platform/android/android_surface_software.cc +++ b/shell/platform/android/android_surface_software.cc @@ -40,11 +40,8 @@ bool GetSkColorType(int32_t buffer_format, AndroidSurfaceSoftware::AndroidSurfaceSoftware( std::shared_ptr android_context, std::shared_ptr jni_facade, - AndroidSurface::Factory surface_factory) - : external_view_embedder_( - std::make_unique(android_context, - jni_facade, - surface_factory)) { + std::shared_ptr external_view_embedder) + : external_view_embedder_(external_view_embedder) { GetSkColorType(WINDOW_FORMAT_RGBA_8888, &target_color_type_, &target_alpha_type_); } diff --git a/shell/platform/android/android_surface_software.h b/shell/platform/android/android_surface_software.h index ec9fdb63f131d..fc54ce2091778 100644 --- a/shell/platform/android/android_surface_software.h +++ b/shell/platform/android/android_surface_software.h @@ -18,9 +18,10 @@ namespace flutter { class AndroidSurfaceSoftware final : public AndroidSurface, public GPUSurfaceSoftwareDelegate { public: - AndroidSurfaceSoftware(std::shared_ptr android_context, - std::shared_ptr jni_facade, - AndroidSurface::Factory surface_factory); + AndroidSurfaceSoftware( + std::shared_ptr android_context, + std::shared_ptr jni_facade, + std::shared_ptr external_view_embedder); ~AndroidSurfaceSoftware() override; @@ -56,7 +57,7 @@ class AndroidSurfaceSoftware final : public AndroidSurface, ExternalViewEmbedder* GetExternalViewEmbedder() override; private: - const std::unique_ptr external_view_embedder_; + const std::shared_ptr external_view_embedder_; sk_sp sk_surface_; fml::RefPtr native_window_; diff --git a/shell/platform/android/android_surface_vulkan.cc b/shell/platform/android/android_surface_vulkan.cc index 4ff32b70a12c5..f31faf3716493 100644 --- a/shell/platform/android/android_surface_vulkan.cc +++ b/shell/platform/android/android_surface_vulkan.cc @@ -15,11 +15,8 @@ namespace flutter { AndroidSurfaceVulkan::AndroidSurfaceVulkan( std::shared_ptr android_context, std::shared_ptr jni_facade, - AndroidSurface::Factory surface_factory) - : external_view_embedder_( - std::make_unique(android_context, - jni_facade, - surface_factory)), + std::shared_ptr external_view_embedder) + : external_view_embedder_(external_view_embedder), proc_table_(fml::MakeRefCounted()) {} AndroidSurfaceVulkan::~AndroidSurfaceVulkan() = default; diff --git a/shell/platform/android/android_surface_vulkan.h b/shell/platform/android/android_surface_vulkan.h index 27b7164e5ba91..b91248b96c76a 100644 --- a/shell/platform/android/android_surface_vulkan.h +++ b/shell/platform/android/android_surface_vulkan.h @@ -21,9 +21,10 @@ namespace flutter { class AndroidSurfaceVulkan : public AndroidSurface, public GPUSurfaceVulkanDelegate { public: - AndroidSurfaceVulkan(std::shared_ptr android_context, - std::shared_ptr jni_facade, - AndroidSurface::Factory surface_factory); + AndroidSurfaceVulkan( + std::shared_ptr android_context, + std::shared_ptr jni_facade, + std::shared_ptr external_view_embedder); ~AndroidSurfaceVulkan() override; @@ -56,8 +57,7 @@ class AndroidSurfaceVulkan : public AndroidSurface, fml::RefPtr vk() override; private: - const std::unique_ptr external_view_embedder_; - + const std::shared_ptr external_view_embedder_; fml::RefPtr proc_table_; fml::RefPtr native_window_; diff --git a/shell/platform/android/apk_asset_provider.cc b/shell/platform/android/apk_asset_provider.cc index 49af5ffa0182a..73a4a26bd1d2d 100644 --- a/shell/platform/android/apk_asset_provider.cc +++ b/shell/platform/android/apk_asset_provider.cc @@ -27,6 +27,10 @@ bool APKAssetProvider::IsValid() const { return true; } +bool APKAssetProvider::IsValidAfterAssetManagerChange() const { + return true; +} + class APKAssetMapping : public fml::Mapping { public: APKAssetMapping(AAsset* asset) : asset_(asset) {} diff --git a/shell/platform/android/apk_asset_provider.h b/shell/platform/android/apk_asset_provider.h index 6ff1e8da7b776..f457b6c4ecc16 100644 --- a/shell/platform/android/apk_asset_provider.h +++ b/shell/platform/android/apk_asset_provider.h @@ -29,6 +29,9 @@ class APKAssetProvider final : public AssetResolver { // |flutter::AssetResolver| bool IsValid() const override; + // |flutter::AssetResolver| + bool IsValidAfterAssetManagerChange() const override; + // |flutter::AssetResolver| std::unique_ptr GetAsMapping( const std::string& asset_name) const override; diff --git a/shell/platform/android/external_view_embedder/BUILD.gn b/shell/platform/android/external_view_embedder/BUILD.gn index a757cd0d2aab1..44bcdaddae280 100644 --- a/shell/platform/android/external_view_embedder/BUILD.gn +++ b/shell/platform/android/external_view_embedder/BUILD.gn @@ -44,6 +44,7 @@ executable("android_external_view_embedder_unittests") { "//flutter/flow", "//flutter/shell/gpu:gpu_surface_gl", "//flutter/shell/platform/android/jni:jni_mock", + "//flutter/shell/platform/android/surface", "//flutter/shell/platform/android/surface:surface_mock", "//flutter/testing", "//flutter/testing:dart", diff --git a/shell/platform/android/external_view_embedder/external_view_embedder.cc b/shell/platform/android/external_view_embedder/external_view_embedder.cc index 5ccd649abab69..6ea00128f5112 100644 --- a/shell/platform/android/external_view_embedder/external_view_embedder.cc +++ b/shell/platform/android/external_view_embedder/external_view_embedder.cc @@ -5,13 +5,14 @@ #include "flutter/shell/platform/android/external_view_embedder/external_view_embedder.h" #include "flutter/fml/trace_event.h" +#include "flutter/shell/platform/android/surface/android_surface.h" namespace flutter { AndroidExternalViewEmbedder::AndroidExternalViewEmbedder( std::shared_ptr android_context, std::shared_ptr jni_facade, - const AndroidSurface::Factory& surface_factory) + std::shared_ptr surface_factory) : ExternalViewEmbedder(), android_context_(android_context), jni_facade_(jni_facade), @@ -265,15 +266,17 @@ void AndroidExternalViewEmbedder::BeginFrame( // The surface size changed. Therefore, destroy existing surfaces as // the existing surfaces in the pool can't be recycled. - if (frame_size_ != frame_size) { + if (frame_size_ != frame_size && raster_thread_merger->IsOnPlatformThread()) { surface_pool_->DestroyLayers(jni_facade_); } - frame_size_ = frame_size; - device_pixel_ratio_ = device_pixel_ratio; + surface_pool_->SetFrameSize(frame_size); // JNI method must be called on the platform thread. if (raster_thread_merger->IsOnPlatformThread()) { jni_facade_->FlutterViewBeginFrame(); } + + frame_size_ = frame_size; + device_pixel_ratio_ = device_pixel_ratio; } // |ExternalViewEmbedder| diff --git a/shell/platform/android/external_view_embedder/external_view_embedder.h b/shell/platform/android/external_view_embedder/external_view_embedder.h index adb5137e2edb1..b8cf718a5ad45 100644 --- a/shell/platform/android/external_view_embedder/external_view_embedder.h +++ b/shell/platform/android/external_view_embedder/external_view_embedder.h @@ -12,6 +12,7 @@ #include "flutter/shell/platform/android/context/android_context.h" #include "flutter/shell/platform/android/external_view_embedder/surface_pool.h" #include "flutter/shell/platform/android/jni/platform_view_android_jni.h" +#include "flutter/shell/platform/android/surface/android_surface.h" #include "third_party/skia/include/core/SkPictureRecorder.h" namespace flutter { @@ -31,7 +32,7 @@ class AndroidExternalViewEmbedder final : public ExternalViewEmbedder { AndroidExternalViewEmbedder( std::shared_ptr android_context, std::shared_ptr jni_facade, - const AndroidSurface::Factory& surface_factory); + std::shared_ptr surface_factory); // |ExternalViewEmbedder| void PrerollCompositeEmbeddedView( @@ -93,7 +94,7 @@ class AndroidExternalViewEmbedder final : public ExternalViewEmbedder { const std::shared_ptr jni_facade_; // Allows to create surfaces. - const AndroidSurface::Factory surface_factory_; + const std::shared_ptr surface_factory_; // Holds surfaces. Allows to recycle surfaces or allocate new ones. const std::unique_ptr surface_pool_; diff --git a/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc b/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc index 5e8e5b6bcb01a..b161a7031d51b 100644 --- a/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc +++ b/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #include "flutter/shell/platform/android/external_view_embedder/external_view_embedder.h" @@ -10,6 +9,7 @@ #include "flutter/fml/raster_thread_merger.h" #include "flutter/fml/thread.h" #include "flutter/shell/platform/android/jni/jni_mock.h" +#include "flutter/shell/platform/android/surface/android_surface.h" #include "flutter/shell/platform/android/surface/android_surface_mock.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -21,6 +21,24 @@ namespace testing { using ::testing::ByMove; using ::testing::Return; +class TestAndroidSurfaceFactory : public AndroidSurfaceFactory { + public: + using TestSurfaceProducer = + std::function(void)>; + explicit TestAndroidSurfaceFactory(TestSurfaceProducer&& surface_producer) { + surface_producer_ = surface_producer; + } + + ~TestAndroidSurfaceFactory() override = default; + + std::unique_ptr CreateSurface() override { + return surface_producer_(); + } + + private: + TestSurfaceProducer surface_producer_; +}; + class SurfaceMock : public Surface { public: MOCK_METHOD(bool, IsValid, (), (override)); @@ -263,10 +281,8 @@ TEST(AndroidExternalViewEmbedder, SubmitFrame) { auto window = fml::MakeRefCounted(nullptr); auto gr_context = GrDirectContext::MakeMock(nullptr); auto frame_size = SkISize::Make(1000, 1000); - auto surface_factory = - [gr_context, window, frame_size]( - std::shared_ptr android_context, - std::shared_ptr jni_facade) { + auto surface_factory = std::make_shared( + [gr_context, window, frame_size]() { auto surface_frame_1 = std::make_unique( SkSurface::MakeNull(1000, 1000), false, [](const SurfaceFrame& surface_frame, SkCanvas* canvas) { @@ -293,7 +309,7 @@ TEST(AndroidExternalViewEmbedder, SubmitFrame) { EXPECT_CALL(*android_surface_mock, SetNativeWindow(window)); return android_surface_mock; - }; + }); auto embedder = std::make_unique( android_context, jni_mock, surface_factory); @@ -482,10 +498,8 @@ TEST(AndroidExternalViewEmbedder, DestroyOverlayLayersOnSizeChange) { auto window = fml::MakeRefCounted(nullptr); auto gr_context = GrDirectContext::MakeMock(nullptr); auto frame_size = SkISize::Make(1000, 1000); - auto surface_factory = - [gr_context, window, frame_size]( - std::shared_ptr android_context, - std::shared_ptr jni_facade) { + auto surface_factory = std::make_shared( + [gr_context, window, frame_size]() { auto surface_frame_1 = std::make_unique( SkSurface::MakeNull(1000, 1000), false, [](const SurfaceFrame& surface_frame, SkCanvas* canvas) { @@ -505,7 +519,7 @@ TEST(AndroidExternalViewEmbedder, DestroyOverlayLayersOnSizeChange) { EXPECT_CALL(*android_surface_mock, SetNativeWindow(window)); return android_surface_mock; - }; + }); auto embedder = std::make_unique( android_context, jni_mock, surface_factory); @@ -557,6 +571,89 @@ TEST(AndroidExternalViewEmbedder, DestroyOverlayLayersOnSizeChange) { raster_thread_merger); } +TEST(AndroidExternalViewEmbedder, DoesNotDestroyOverlayLayersOnSizeChange) { + auto jni_mock = std::make_shared(); + auto android_context = + std::make_shared(AndroidRenderingAPI::kSoftware); + + auto window = fml::MakeRefCounted(nullptr); + auto gr_context = GrDirectContext::MakeMock(nullptr); + auto frame_size = SkISize::Make(1000, 1000); + auto surface_factory = std::make_shared( + [gr_context, window, frame_size]() { + auto surface_frame_1 = std::make_unique( + SkSurface::MakeNull(1000, 1000), false, + [](const SurfaceFrame& surface_frame, SkCanvas* canvas) { + return true; + }); + + auto surface_mock = std::make_unique(); + EXPECT_CALL(*surface_mock, AcquireFrame(frame_size)) + .WillOnce(Return(ByMove(std::move(surface_frame_1)))); + + auto android_surface_mock = std::make_unique(); + EXPECT_CALL(*android_surface_mock, IsValid()).WillOnce(Return(true)); + + EXPECT_CALL(*android_surface_mock, CreateGPUSurface(gr_context.get())) + .WillOnce(Return(ByMove(std::move(surface_mock)))); + + EXPECT_CALL(*android_surface_mock, SetNativeWindow(window)); + + return android_surface_mock; + }); + + auto embedder = std::make_unique( + android_context, jni_mock, surface_factory); + + // ------------------ First frame ------------------ // + { + auto raster_thread_merger = GetThreadMergerFromPlatformThread(); + EXPECT_CALL(*jni_mock, FlutterViewBeginFrame()); + embedder->BeginFrame(frame_size, nullptr, 1.5, raster_thread_merger); + + // Add an Android view. + MutatorsStack stack1; + // TODO(egarciad): Investigate why Flow applies the device pixel ratio to + // the offsetPixels, but not the sizePoints. + auto view_params_1 = std::make_unique( + SkMatrix(), SkSize::Make(200, 200), stack1); + + embedder->PrerollCompositeEmbeddedView(0, std::move(view_params_1)); + + // This simulates Flutter UI that intersects with the Android view. + embedder->CompositeEmbeddedView(0)->drawRect( + SkRect::MakeXYWH(50, 50, 200, 200), SkPaint()); + + // Create a new overlay surface. + EXPECT_CALL(*jni_mock, FlutterViewCreateOverlaySurface()) + .WillOnce(Return( + ByMove(std::make_unique( + 0, window)))); + // The JNI call to display the Android view. + EXPECT_CALL(*jni_mock, FlutterViewOnDisplayPlatformView(0, 0, 0, 200, 200, + 300, 300, stack1)); + EXPECT_CALL(*jni_mock, + FlutterViewDisplayOverlaySurface(0, 50, 50, 200, 200)); + + auto surface_frame = + std::make_unique(SkSurface::MakeNull(1000, 1000), false, + [](const SurfaceFrame& surface_frame, + SkCanvas* canvas) { return true; }); + embedder->SubmitFrame(gr_context.get(), std::move(surface_frame)); + + EXPECT_CALL(*jni_mock, FlutterViewEndFrame()); + embedder->EndFrame(/*should_resubmit_frame=*/false, raster_thread_merger); + } + + // Changing the frame size from the raster thread does not make JNI calls. + + EXPECT_CALL(*jni_mock, FlutterViewDestroyOverlaySurfaces()).Times(0); + EXPECT_CALL(*jni_mock, FlutterViewBeginFrame()).Times(0); + + embedder->BeginFrame(SkISize::Make(30, 40), nullptr, 1.0, + GetThreadMergerFromRasterThread()); +} + TEST(AndroidExternalViewEmbedder, SupportsDynamicThreadMerging) { auto jni_mock = std::make_shared(); diff --git a/shell/platform/android/external_view_embedder/surface_pool.cc b/shell/platform/android/external_view_embedder/surface_pool.cc index bee34109b95b1..b07a8d1ec27c6 100644 --- a/shell/platform/android/external_view_embedder/surface_pool.cc +++ b/shell/platform/android/external_view_embedder/surface_pool.cc @@ -23,12 +23,17 @@ std::shared_ptr SurfacePool::GetLayer( GrDirectContext* gr_context, std::shared_ptr android_context, std::shared_ptr jni_facade, - const AndroidSurface::Factory& surface_factory) { + std::shared_ptr surface_factory) { + // Destroy current layers in the pool if the frame size has changed. + if (requested_frame_size_ != current_frame_size_) { + DestroyLayers(jni_facade); + } + intptr_t gr_context_key = reinterpret_cast(gr_context); // Allocate a new surface if there isn't one available. if (available_layer_index_ >= layers_.size()) { std::unique_ptr android_surface = - surface_factory(android_context, jni_facade); + surface_factory->CreateSurface(); FML_CHECK(android_surface && android_surface->IsValid()) << "Could not create an OpenGL, Vulkan or Software surface to setup " @@ -63,6 +68,7 @@ std::shared_ptr SurfacePool::GetLayer( layer->surface = std::move(surface); } available_layer_index_++; + current_frame_size_ = requested_frame_size_; return layer; } @@ -87,4 +93,8 @@ std::vector> SurfacePool::GetUnusedLayers() { return results; } +void SurfacePool::SetFrameSize(SkISize frame_size) { + requested_frame_size_ = frame_size; +} + } // namespace flutter diff --git a/shell/platform/android/external_view_embedder/surface_pool.h b/shell/platform/android/external_view_embedder/surface_pool.h index 190119c75cf25..afdb6b6a35b9c 100644 --- a/shell/platform/android/external_view_embedder/surface_pool.h +++ b/shell/platform/android/external_view_embedder/surface_pool.h @@ -55,7 +55,7 @@ class SurfacePool { GrDirectContext* gr_context, std::shared_ptr android_context, std::shared_ptr jni_facade, - const AndroidSurface::Factory& surface_factory); + std::shared_ptr surface_factory); // Gets the layers in the pool that aren't currently used. // This method doesn't mark the layers as unused. @@ -67,6 +67,11 @@ class SurfacePool { // Destroys all the layers in the pool. void DestroyLayers(std::shared_ptr jni_facade); + // Sets the frame size used by the layers in the pool. + // If the current layers in the pool have a different frame size, + // then they are deallocated as soon as |GetLayer| is called. + void SetFrameSize(SkISize frame_size); + private: // The index of the entry in the layers_ vector that determines the beginning // of the unused layers. For example, consider the following vector: @@ -81,7 +86,15 @@ class SurfacePool { // This indicates that entries starting from 1 can be reused meanwhile the // entry at position 0 cannot be reused. size_t available_layer_index_ = 0; + + // The layers in the pool. std::vector> layers_; + + // The frame size of the layers in the pool. + SkISize current_frame_size_; + + // The frame size to be used by future layers. + SkISize requested_frame_size_; }; } // namespace flutter diff --git a/shell/platform/android/external_view_embedder/surface_pool_unittests.cc b/shell/platform/android/external_view_embedder/surface_pool_unittests.cc index 18ebb9a285fcf..c313a43eb1254 100644 --- a/shell/platform/android/external_view_embedder/surface_pool_unittests.cc +++ b/shell/platform/android/external_view_embedder/surface_pool_unittests.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #include "flutter/shell/platform/android/external_view_embedder/surface_pool.h" @@ -18,6 +17,24 @@ namespace testing { using ::testing::ByMove; using ::testing::Return; +class TestAndroidSurfaceFactory : public AndroidSurfaceFactory { + public: + using TestSurfaceProducer = + std::function(void)>; + explicit TestAndroidSurfaceFactory(TestSurfaceProducer&& surface_producer) { + surface_producer_ = surface_producer; + } + + ~TestAndroidSurfaceFactory() override = default; + + std::unique_ptr CreateSurface() override { + return surface_producer_(); + } + + private: + TestSurfaceProducer surface_producer_; +}; + TEST(SurfacePool, GetLayer__AllocateOneLayer) { auto pool = std::make_unique(); @@ -33,14 +50,13 @@ TEST(SurfacePool, GetLayer__AllocateOneLayer) { 0, window)))); auto surface_factory = - [gr_context, window](std::shared_ptr android_context, - std::shared_ptr jni_facade) { + std::make_shared([gr_context, window]() { auto android_surface_mock = std::make_unique(); EXPECT_CALL(*android_surface_mock, CreateGPUSurface(gr_context.get())); EXPECT_CALL(*android_surface_mock, SetNativeWindow(window)); EXPECT_CALL(*android_surface_mock, IsValid()).WillOnce(Return(true)); return android_surface_mock; - }; + }); auto layer = pool->GetLayer(gr_context.get(), android_context, jni_mock, surface_factory); @@ -64,14 +80,13 @@ TEST(SurfacePool, GetUnusedLayers) { 0, window)))); auto surface_factory = - [gr_context, window](std::shared_ptr android_context, - std::shared_ptr jni_facade) { + std::make_shared([gr_context, window]() { auto android_surface_mock = std::make_unique(); EXPECT_CALL(*android_surface_mock, CreateGPUSurface(gr_context.get())); EXPECT_CALL(*android_surface_mock, SetNativeWindow(window)); EXPECT_CALL(*android_surface_mock, IsValid()).WillOnce(Return(true)); return android_surface_mock; - }; + }); auto layer = pool->GetLayer(gr_context.get(), android_context, jni_mock, surface_factory); ASSERT_EQ(0UL, pool->GetUnusedLayers().size()); @@ -97,10 +112,8 @@ TEST(SurfacePool, GetLayer__Recycle) { std::make_shared(AndroidRenderingAPI::kSoftware); auto gr_context_2 = GrDirectContext::MakeMock(nullptr); - auto surface_factory = - [gr_context_1, gr_context_2, window]( - std::shared_ptr android_context, - std::shared_ptr jni_facade) { + auto surface_factory = std::make_shared( + [gr_context_1, gr_context_2, window]() { auto android_surface_mock = std::make_unique(); // Allocate two GPU surfaces for each gr context. EXPECT_CALL(*android_surface_mock, @@ -111,7 +124,7 @@ TEST(SurfacePool, GetLayer__Recycle) { EXPECT_CALL(*android_surface_mock, SetNativeWindow(window)); EXPECT_CALL(*android_surface_mock, IsValid()).WillOnce(Return(true)); return android_surface_mock; - }; + }); auto layer_1 = pool->GetLayer(gr_context_1.get(), android_context, jni_mock, surface_factory); @@ -147,14 +160,13 @@ TEST(SurfacePool, GetLayer__AllocateTwoLayers) { 1, window)))); auto surface_factory = - [gr_context, window](std::shared_ptr android_context, - std::shared_ptr jni_facade) { + std::make_shared([gr_context, window]() { auto android_surface_mock = std::make_unique(); EXPECT_CALL(*android_surface_mock, CreateGPUSurface(gr_context.get())); EXPECT_CALL(*android_surface_mock, SetNativeWindow(window)); EXPECT_CALL(*android_surface_mock, IsValid()).WillOnce(Return(true)); return android_surface_mock; - }; + }); auto layer_1 = pool->GetLayer(gr_context.get(), android_context, jni_mock, surface_factory); auto layer_2 = pool->GetLayer(gr_context.get(), android_context, jni_mock, @@ -185,14 +197,13 @@ TEST(SurfacePool, DestroyLayers) { 0, window)))); auto surface_factory = - [gr_context, window](std::shared_ptr android_context, - std::shared_ptr jni_facade) { + std::make_shared([gr_context, window]() { auto android_surface_mock = std::make_unique(); EXPECT_CALL(*android_surface_mock, CreateGPUSurface(gr_context.get())); EXPECT_CALL(*android_surface_mock, SetNativeWindow(window)); EXPECT_CALL(*android_surface_mock, IsValid()).WillOnce(Return(true)); return android_surface_mock; - }; + }); pool->GetLayer(gr_context.get(), android_context, jni_mock, surface_factory); EXPECT_CALL(*jni_mock, FlutterViewDestroyOverlaySurfaces()); @@ -202,5 +213,45 @@ TEST(SurfacePool, DestroyLayers) { ASSERT_TRUE(pool->GetUnusedLayers().empty()); } +TEST(SurfacePool, DestroyLayers__frameSizeChanged) { + auto pool = std::make_unique(); + auto jni_mock = std::make_shared(); + + auto gr_context = GrDirectContext::MakeMock(nullptr); + auto android_context = + std::make_shared(AndroidRenderingAPI::kSoftware); + + auto window = fml::MakeRefCounted(nullptr); + + auto surface_factory = + std::make_shared([gr_context, window]() { + auto android_surface_mock = std::make_unique(); + EXPECT_CALL(*android_surface_mock, CreateGPUSurface(gr_context.get())); + EXPECT_CALL(*android_surface_mock, SetNativeWindow(window)); + EXPECT_CALL(*android_surface_mock, IsValid()).WillOnce(Return(true)); + return android_surface_mock; + }); + pool->SetFrameSize(SkISize::Make(10, 10)); + EXPECT_CALL(*jni_mock, FlutterViewDestroyOverlaySurfaces()).Times(0); + EXPECT_CALL(*jni_mock, FlutterViewCreateOverlaySurface()) + .Times(1) + .WillOnce(Return( + ByMove(std::make_unique( + 0, window)))); + + pool->GetLayer(gr_context.get(), android_context, jni_mock, surface_factory); + + pool->SetFrameSize(SkISize::Make(20, 20)); + EXPECT_CALL(*jni_mock, FlutterViewDestroyOverlaySurfaces()).Times(1); + EXPECT_CALL(*jni_mock, FlutterViewCreateOverlaySurface()) + .Times(1) + .WillOnce(Return( + ByMove(std::make_unique( + 1, window)))); + pool->GetLayer(gr_context.get(), android_context, jni_mock, surface_factory); + + ASSERT_TRUE(pool->GetUnusedLayers().empty()); +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/android/io/flutter/Log.java b/shell/platform/android/io/flutter/Log.java index 2fb8d029e79a5..fa31b8476a0f5 100644 --- a/shell/platform/android/io/flutter/Log.java +++ b/shell/platform/android/io/flutter/Log.java @@ -13,6 +13,13 @@ public class Log { private static int logLevel = android.util.Log.DEBUG; + public static int ASSERT = android.util.Log.ASSERT; + public static int DEBUG = android.util.Log.DEBUG; + public static int ERROR = android.util.Log.ERROR; + public static int INFO = android.util.Log.INFO; + public static int VERBOSE = android.util.Log.VERBOSE; + public static int WARN = android.util.Log.WARN; + /** * Sets a log cutoff such that a log level of lower priority than {@code logLevel} is filtered * out. @@ -23,6 +30,12 @@ public static void setLogLevel(int logLevel) { Log.logLevel = logLevel; } + public static void println(@NonNull int level, @NonNull String tag, @NonNull String message) { + if (BuildConfig.DEBUG && logLevel <= level) { + android.util.Log.println(level, tag, message); + } + } + public static void v(@NonNull String tag, @NonNull String message) { if (BuildConfig.DEBUG && logLevel <= android.util.Log.VERBOSE) { android.util.Log.v(tag, message); diff --git a/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java b/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java index 970f09775a385..e337f373f7ebe 100644 --- a/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java +++ b/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java @@ -19,12 +19,12 @@ import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; -import android.util.Log; import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager.LayoutParams; +import io.flutter.Log; import io.flutter.plugin.common.PluginRegistry; import io.flutter.plugin.platform.PlatformPlugin; import io.flutter.util.Preconditions; diff --git a/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java b/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java index 14d7effe56c11..7de46d4d9ebdb 100644 --- a/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java +++ b/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java @@ -4,12 +4,12 @@ package io.flutter.embedding.android; -import android.util.Log; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import io.flutter.Log; import io.flutter.embedding.engine.systemchannels.KeyEventChannel; import io.flutter.plugin.editing.TextInputPlugin; import java.util.AbstractMap.SimpleImmutableEntry; diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java index 59fa7132e3008..5e9864c345bdc 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java @@ -23,6 +23,7 @@ import io.flutter.embedding.engine.mutatorsstack.FlutterMutatorsStack; import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener; import io.flutter.embedding.engine.renderer.RenderSurface; +import io.flutter.embedding.engine.renderer.SurfaceTextureWrapper; import io.flutter.plugin.common.StandardMessageCodec; import io.flutter.plugin.localization.LocalizationPlugin; import io.flutter.plugin.platform.PlatformViewsController; @@ -581,14 +582,14 @@ public void setAccessibilityFeatures(int flags) { * within Flutter's UI. */ @UiThread - public void registerTexture(long textureId, @NonNull SurfaceTexture surfaceTexture) { + public void registerTexture(long textureId, @NonNull SurfaceTextureWrapper textureWrapper) { ensureRunningOnMainThread(); ensureAttachedToNative(); - nativeRegisterTexture(nativePlatformViewId, textureId, surfaceTexture); + nativeRegisterTexture(nativePlatformViewId, textureId, textureWrapper); } private native void nativeRegisterTexture( - long nativePlatformViewId, long textureId, @NonNull SurfaceTexture surfaceTexture); + long nativePlatformViewId, long textureId, @NonNull SurfaceTextureWrapper textureWrapper); /** * Call this method to inform Flutter that a texture previously registered with {@link diff --git a/shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java b/shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java index 2c4726b7f8fd3..fc03bb1f3c6f6 100644 --- a/shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java +++ b/shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java @@ -86,6 +86,8 @@ public void handleMessageFromDart( } catch (Exception ex) { Log.e(TAG, "Uncaught exception in binary message listener", ex); flutterJNI.invokePlatformMessageEmptyResponseCallback(replyId); + } catch (Error err) { + handleError(err); } } else { Log.v(TAG, "No registered handler for message. Responding to Dart with empty reply message."); @@ -103,6 +105,8 @@ public void handlePlatformMessageResponse(int replyId, @Nullable byte[] reply) { callback.reply(reply == null ? null : ByteBuffer.wrap(reply)); } catch (Exception ex) { Log.e(TAG, "Uncaught exception in binary message reply handler", ex); + } catch (Error err) { + handleError(err); } } } @@ -123,7 +127,19 @@ public int getPendingChannelResponseCount() { return pendingReplies.size(); } - private static class Reply implements BinaryMessenger.BinaryReply { + // Handles `Error` objects which are not supposed to be caught. + // + // We forward them to the thread's uncaught exception handler if there is one. If not, they + // are rethrown. + private static void handleError(Error err) { + Thread currentThread = Thread.currentThread(); + if (currentThread.getUncaughtExceptionHandler() == null) { + throw err; + } + currentThread.getUncaughtExceptionHandler().uncaughtException(currentThread, err); + } + + static class Reply implements BinaryMessenger.BinaryReply { @NonNull private final FlutterJNI flutterJNI; private final int replyId; private final AtomicBoolean done = new AtomicBoolean(false); diff --git a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java index 2b7c0879bcb76..e2d97c40ee174 100644 --- a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java +++ b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java @@ -10,12 +10,12 @@ import android.os.Handler; import android.os.Looper; import android.os.SystemClock; -import android.util.Log; import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.flutter.BuildConfig; import io.flutter.FlutterInjector; +import io.flutter.Log; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.util.PathUtils; import io.flutter.view.VsyncWaiter; diff --git a/shell/platform/android/io/flutter/embedding/engine/loader/ResourceExtractor.java b/shell/platform/android/io/flutter/embedding/engine/loader/ResourceExtractor.java index f8714dc813fcd..a0b7382edf3da 100644 --- a/shell/platform/android/io/flutter/embedding/engine/loader/ResourceExtractor.java +++ b/shell/platform/android/io/flutter/embedding/engine/loader/ResourceExtractor.java @@ -11,10 +11,10 @@ import android.content.res.AssetManager; import android.os.AsyncTask; import android.os.Build; -import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import io.flutter.BuildConfig; +import io.flutter.Log; import java.io.*; import java.util.ArrayList; import java.util.Collection; diff --git a/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java b/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java index 0aa11c4f2aacf..20a72e81fa623 100644 --- a/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java +++ b/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java @@ -99,18 +99,18 @@ public SurfaceTextureEntry createSurfaceTexture() { final SurfaceTextureRegistryEntry entry = new SurfaceTextureRegistryEntry(nextTextureId.getAndIncrement(), surfaceTexture); Log.v(TAG, "New SurfaceTexture ID: " + entry.id()); - registerTexture(entry.id(), surfaceTexture); + registerTexture(entry.id(), entry.textureWrapper()); return entry; } final class SurfaceTextureRegistryEntry implements TextureRegistry.SurfaceTextureEntry { private final long id; - @NonNull private final SurfaceTexture surfaceTexture; + @NonNull private final SurfaceTextureWrapper textureWrapper; private boolean released; SurfaceTextureRegistryEntry(long id, @NonNull SurfaceTexture surfaceTexture) { this.id = id; - this.surfaceTexture = surfaceTexture; + this.textureWrapper = new SurfaceTextureWrapper(surfaceTexture); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // The callback relies on being executed on the UI thread (unsynchronised read of @@ -118,12 +118,12 @@ final class SurfaceTextureRegistryEntry implements TextureRegistry.SurfaceTextur // and also the engine code check for platform thread in // Shell::OnPlatformViewMarkTextureFrameAvailable), // so we explicitly pass a Handler for the current thread. - this.surfaceTexture.setOnFrameAvailableListener(onFrameListener, new Handler()); + this.surfaceTexture().setOnFrameAvailableListener(onFrameListener, new Handler()); } else { // Android documentation states that the listener can be called on an arbitrary thread. // But in practice, versions of Android that predate the newer API will call the listener // on the thread where the SurfaceTexture was constructed. - this.surfaceTexture.setOnFrameAvailableListener(onFrameListener); + this.surfaceTexture().setOnFrameAvailableListener(onFrameListener); } } @@ -142,10 +142,15 @@ public void onFrameAvailable(@NonNull SurfaceTexture texture) { } }; + @NonNull + public SurfaceTextureWrapper textureWrapper() { + return textureWrapper; + } + @Override @NonNull public SurfaceTexture surfaceTexture() { - return surfaceTexture; + return textureWrapper.surfaceTexture(); } @Override @@ -159,7 +164,7 @@ public void release() { return; } Log.v(TAG, "Releasing a SurfaceTexture (" + id + ")."); - surfaceTexture.release(); + textureWrapper.release(); unregisterTexture(id); released = true; } @@ -298,8 +303,8 @@ public void dispatchPointerDataPacket(@NonNull ByteBuffer buffer, int position) } // TODO(mattcarroll): describe the native behavior that this invokes - private void registerTexture(long textureId, @NonNull SurfaceTexture surfaceTexture) { - flutterJNI.registerTexture(textureId, surfaceTexture); + private void registerTexture(long textureId, @NonNull SurfaceTextureWrapper textureWrapper) { + flutterJNI.registerTexture(textureId, textureWrapper); } // TODO(mattcarroll): describe the native behavior that this invokes diff --git a/shell/platform/android/io/flutter/embedding/engine/renderer/SurfaceTextureWrapper.java b/shell/platform/android/io/flutter/embedding/engine/renderer/SurfaceTextureWrapper.java new file mode 100644 index 0000000000000..478d997a5efad --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/renderer/SurfaceTextureWrapper.java @@ -0,0 +1,69 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.embedding.engine.renderer; + +import android.graphics.SurfaceTexture; +import androidx.annotation.Keep; +import androidx.annotation.NonNull; + +/** + * A wrapper for a SurfaceTexture that tracks whether the texture has been released. + * + *

The engine calls {@code SurfaceTexture.release} on the platform thread, but {@code + * updateTexImage} is called on the raster thread. This wrapper will prevent {@code updateTexImage} + * calls on an abandoned texture. + */ +@Keep +public class SurfaceTextureWrapper { + private SurfaceTexture surfaceTexture; + private boolean released; + + public SurfaceTextureWrapper(@NonNull SurfaceTexture surfaceTexture) { + this.surfaceTexture = surfaceTexture; + this.released = false; + } + + @NonNull + public SurfaceTexture surfaceTexture() { + return surfaceTexture; + } + + // Called by native. + @SuppressWarnings("unused") + public void updateTexImage() { + synchronized (this) { + if (!released) { + surfaceTexture.updateTexImage(); + } + } + } + + public void release() { + synchronized (this) { + if (!released) { + surfaceTexture.release(); + released = true; + } + } + } + + // Called by native. + @SuppressWarnings("unused") + public void attachToGLContext(int texName) { + surfaceTexture.attachToGLContext(texName); + } + + // Called by native. + @SuppressWarnings("unused") + public void detachFromGLContext() { + surfaceTexture.detachFromGLContext(); + } + + // Called by native. + @SuppressWarnings("unused") + public void getTransformMatrix(float[] mtx) { + surfaceTexture.getTransformMatrix(mtx); + } +} diff --git a/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java b/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java index 03fc178498d8e..d94b90c47b86b 100644 --- a/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java +++ b/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java @@ -4,11 +4,11 @@ package io.flutter.plugin.common; -import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import io.flutter.BuildConfig; +import io.flutter.Log; import io.flutter.plugin.common.BinaryMessenger.BinaryMessageHandler; import io.flutter.plugin.common.BinaryMessenger.BinaryReply; import java.nio.ByteBuffer; diff --git a/shell/platform/android/io/flutter/plugin/common/ErrorLogResult.java b/shell/platform/android/io/flutter/plugin/common/ErrorLogResult.java index d4acc77ea0f7c..90b1c541df39c 100644 --- a/shell/platform/android/io/flutter/plugin/common/ErrorLogResult.java +++ b/shell/platform/android/io/flutter/plugin/common/ErrorLogResult.java @@ -4,9 +4,9 @@ package io.flutter.plugin.common; -import android.util.Log; import androidx.annotation.Nullable; import io.flutter.BuildConfig; +import io.flutter.Log; /** * An implementation of {@link MethodChannel.Result} that writes error results to the Android log. diff --git a/shell/platform/android/io/flutter/plugin/common/EventChannel.java b/shell/platform/android/io/flutter/plugin/common/EventChannel.java index f7d2e3e84c2f2..fed8b053e93ed 100644 --- a/shell/platform/android/io/flutter/plugin/common/EventChannel.java +++ b/shell/platform/android/io/flutter/plugin/common/EventChannel.java @@ -4,9 +4,9 @@ package io.flutter.plugin.common; -import android.util.Log; import androidx.annotation.UiThread; import io.flutter.BuildConfig; +import io.flutter.Log; import io.flutter.plugin.common.BinaryMessenger.BinaryMessageHandler; import io.flutter.plugin.common.BinaryMessenger.BinaryReply; import java.nio.ByteBuffer; diff --git a/shell/platform/android/io/flutter/plugin/common/FlutterException.java b/shell/platform/android/io/flutter/plugin/common/FlutterException.java index 8748efdc223b5..ffcce9eb046b5 100644 --- a/shell/platform/android/io/flutter/plugin/common/FlutterException.java +++ b/shell/platform/android/io/flutter/plugin/common/FlutterException.java @@ -4,8 +4,8 @@ package io.flutter.plugin.common; -import android.util.Log; import io.flutter.BuildConfig; +import io.flutter.Log; /** Thrown to indicate that a Flutter method invocation failed on the Flutter side. */ public class FlutterException extends RuntimeException { diff --git a/shell/platform/android/io/flutter/plugin/common/MethodChannel.java b/shell/platform/android/io/flutter/plugin/common/MethodChannel.java index 81e50e3b938c3..8b1336a7e63c9 100644 --- a/shell/platform/android/io/flutter/plugin/common/MethodChannel.java +++ b/shell/platform/android/io/flutter/plugin/common/MethodChannel.java @@ -4,11 +4,11 @@ package io.flutter.plugin.common; -import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import io.flutter.BuildConfig; +import io.flutter.Log; import io.flutter.plugin.common.BinaryMessenger.BinaryMessageHandler; import io.flutter.plugin.common.BinaryMessenger.BinaryReply; import java.io.PrintWriter; diff --git a/shell/platform/android/io/flutter/plugin/common/StandardMessageCodec.java b/shell/platform/android/io/flutter/plugin/common/StandardMessageCodec.java index 9a99de87bf116..625f522cac0a2 100644 --- a/shell/platform/android/io/flutter/plugin/common/StandardMessageCodec.java +++ b/shell/platform/android/io/flutter/plugin/common/StandardMessageCodec.java @@ -4,8 +4,8 @@ package io.flutter.plugin.common; -import android.util.Log; import io.flutter.BuildConfig; +import io.flutter.Log; import java.io.ByteArrayOutputStream; import java.math.BigInteger; import java.nio.ByteBuffer; diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java b/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java index 7237576790394..3f18d9472ede5 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java @@ -17,7 +17,9 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import io.flutter.Log; import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import java.io.FileNotFoundException; import java.util.List; /** Android implementation of the platform plugin. */ @@ -29,6 +31,7 @@ public class PlatformPlugin { private final PlatformChannel platformChannel; private PlatformChannel.SystemChromeStyle currentTheme; private int mEnabledOverlays; + private static final String TAG = "PlatformPlugin"; @VisibleForTesting final PlatformChannel.PlatformMessageHandler mPlatformMessageHandler = @@ -283,11 +286,25 @@ private CharSequence getClipboardData(PlatformChannel.ClipboardContentFormat for if (!clipboard.hasPrimaryClip()) return null; - ClipData clip = clipboard.getPrimaryClip(); - if (clip == null) return null; - - if (format == null || format == PlatformChannel.ClipboardContentFormat.PLAIN_TEXT) { - return clip.getItemAt(0).coerceToText(activity); + try { + ClipData clip = clipboard.getPrimaryClip(); + if (clip == null) return null; + if (format == null || format == PlatformChannel.ClipboardContentFormat.PLAIN_TEXT) { + ClipData.Item item = clip.getItemAt(0); + if (item.getUri() != null) + activity.getContentResolver().openTypedAssetFileDescriptor(item.getUri(), "text/*", null); + return item.coerceToText(activity); + } + } catch (SecurityException e) { + Log.w( + TAG, + "Attempted to get clipboard data that requires additional permission(s).\n" + + "See the exception details for which permission(s) are required, and consider adding them to your Android Manifest as described in:\n" + + "https://developer.android.com/guide/topics/permissions/overview", + e); + return null; + } catch (FileNotFoundException e) { + return null; } return null; diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index c7de3c2c0ec6c..b000a903c956d 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -11,15 +11,16 @@ import android.content.Context; import android.os.Build; import android.util.DisplayMetrics; -import android.util.Log; import android.util.SparseArray; import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.annotation.VisibleForTesting; +import io.flutter.Log; import io.flutter.embedding.android.AndroidTouchProcessor; import io.flutter.embedding.android.FlutterImageView; import io.flutter.embedding.android.FlutterView; @@ -83,7 +84,7 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega // The views returned by `PlatformView#getView()`. // // This only applies to hybrid composition. - private final SparseArray platformViews; + private final SparseArray platformViews; // The platform view parents that are appended to `FlutterView`. // If an entry in `platformViews` doesn't have an entry in this array, the platform view isn't @@ -143,32 +144,24 @@ public void createAndroidViewForPlatformView( } final PlatformView platformView = factory.create(context, request.viewId, createParams); - final View view = platformView.getView(); - if (view == null) { - throw new IllegalStateException( - "PlatformView#getView() returned null, but an Android view reference was expected."); - } - if (view.getParent() != null) { - throw new IllegalStateException( - "The Android view returned from PlatformView#getView() was already added to a parent view."); - } - platformViews.put(request.viewId, view); + platformViews.put(request.viewId, platformView); } @Override public void disposeAndroidViewForPlatformView(int viewId) { // Hybrid view. - final View platformView = platformViews.get(viewId); + final PlatformView platformView = platformViews.get(viewId); final FlutterMutatorView parentView = platformViewParent.get(viewId); if (platformView != null) { if (parentView != null) { - parentView.removeView(platformView); + parentView.removeView(platformView.getView()); } platformViews.remove(viewId); + platformView.dispose(); } if (parentView != null) { - ((FlutterView) flutterView).removeView(parentView); + ((ViewGroup) parentView.getParent()).removeView(parentView); platformViewParent.remove(viewId); } } @@ -311,8 +304,10 @@ public void onTouch(@NonNull PlatformViewsChannel.PlatformViewTouch touch) { vdControllers.get(touch.viewId).dispatchTouchEvent(event); } else if (platformViews.get(viewId) != null) { final MotionEvent event = toMotionEvent(density, touch, /*usingVirtualDiplays=*/ false); - View view = platformViews.get(touch.viewId); - view.dispatchTouchEvent(event); + View view = platformViews.get(touch.viewId).getView(); + if (view != null) { + view.dispatchTouchEvent(event); + } } else { throw new IllegalStateException("Sending touch to an unknown view with id: " + viewId); } @@ -580,7 +575,7 @@ public void onPreEngineRestart() { public View getPlatformViewById(Integer id) { // Hybrid composition. if (platformViews.get(id) != null) { - return platformViews.get(id); + return platformViews.get(id).getView(); } VirtualDisplayController controller = vdControllers.get(id); if (controller == null) { @@ -690,6 +685,10 @@ private void flushAllViews() { controller.dispose(); } vdControllers.clear(); + + while (platformViews.size() > 0) { + channelHandler.disposeAndroidViewForPlatformView(platformViews.keyAt(0)); + } } private void initializeRootImageViewIfNeeded() { @@ -701,19 +700,27 @@ private void initializeRootImageViewIfNeeded() { @VisibleForTesting void initializePlatformViewIfNeeded(int viewId) { - final View view = platformViews.get(viewId); - if (view == null) { + final PlatformView platformView = platformViews.get(viewId); + if (platformView == null) { throw new IllegalStateException( "Platform view hasn't been initialized from the platform view channel."); } if (platformViewParent.get(viewId) != null) { return; } + if (platformView.getView() == null) { + throw new IllegalStateException( + "PlatformView#getView() returned null, but an Android view reference was expected."); + } + if (platformView.getView().getParent() != null) { + throw new IllegalStateException( + "The Android view returned from PlatformView#getView() was already added to a parent view."); + } final FlutterMutatorView parentView = new FlutterMutatorView( context, context.getResources().getDisplayMetrics().density, androidTouchProcessor); platformViewParent.put(viewId, parentView); - parentView.addView(view); + parentView.addView(platformView.getView()); ((FlutterView) flutterView).addView(parentView); } @@ -740,9 +747,11 @@ public void onDisplayPlatformView( final FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(viewWidth, viewHeight); - final View platformView = platformViews.get(viewId); - platformView.setLayoutParams(layoutParams); - platformView.bringToFront(); + final View view = platformViews.get(viewId).getView(); + if (view != null) { + view.setLayoutParams(layoutParams); + view.bringToFront(); + } currentFrameUsedPlatformViewIds.add(viewId); } diff --git a/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java b/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java index 993f88efb2a8c..b40ef87e5ccc7 100644 --- a/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java +++ b/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java @@ -17,7 +17,6 @@ import android.graphics.drawable.ColorDrawable; import android.os.Build; import android.os.Bundle; -import android.util.Log; import android.view.Display; import android.view.Gravity; import android.view.View; @@ -29,6 +28,7 @@ import androidx.annotation.Keep; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import io.flutter.Log; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index d7bdf3926432e..92cbe68b196f2 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -15,7 +15,6 @@ import android.os.Bundle; import android.os.Handler; import android.provider.Settings; -import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.WindowInsets; @@ -28,6 +27,8 @@ import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import io.flutter.BuildConfig; +import io.flutter.Log; +import io.flutter.embedding.android.FlutterActivity; import io.flutter.embedding.engine.systemchannels.AccessibilityChannel; import io.flutter.plugin.platform.PlatformViewsAccessibilityDelegate; import io.flutter.util.Predicate; @@ -541,12 +542,22 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { return null; } + // Generate accessibility node for platform views using a virtual display. + // + // In this case, register the accessibility node in the view embedder, + // so the accessibility tree can be mirrored as a subtree of the Flutter accessibility tree. + // This is in constrast to hybrid composition where the embeded view is in the view hiearchy, + // so it doesn't need to be mirrored. + // + // See the case down below for how hybrid composition is handled. if (semanticsNode.platformViewId != -1) { - // For platform views we delegate the node creation to the accessibility view embedder. View embeddedView = platformViewsAccessibilityDelegate.getPlatformViewById(semanticsNode.platformViewId); - Rect bounds = semanticsNode.getGlobalRect(); - return accessibilityViewEmbedder.getRootNode(embeddedView, semanticsNode.id, bounds); + boolean childUsesVirtualDisplay = !(embeddedView.getContext() instanceof FlutterActivity); + if (childUsesVirtualDisplay) { + Rect bounds = semanticsNode.getGlobalRect(); + return accessibilityViewEmbedder.getRootNode(embeddedView, semanticsNode.id, bounds); + } } AccessibilityNodeInfo result = @@ -823,11 +834,28 @@ && shouldSetCollectionInfo(semanticsNode)) { } for (SemanticsNode child : semanticsNode.childrenInTraversalOrder) { - if (!child.hasFlag(Flag.IS_HIDDEN)) { - result.addChild(rootAccessibilityView, child.id); + if (child.hasFlag(Flag.IS_HIDDEN)) { + continue; + } + if (child.platformViewId != -1) { + View embeddedView = + platformViewsAccessibilityDelegate.getPlatformViewById(child.platformViewId); + + // Add the embeded view as a child of the current accessibility node if it's using + // hybrid composition. + // + // In this case, the view is in the Activity's view hierarchy, so it doesn't need to be + // mirrored. + // + // See the case above for how virtual displays are handled. + boolean childUsesHybridComposition = embeddedView.getContext() instanceof FlutterActivity; + if (childUsesHybridComposition) { + result.addChild(embeddedView); + continue; + } } + result.addChild(rootAccessibilityView, child.id); } - return result; } diff --git a/shell/platform/android/io/flutter/view/AccessibilityViewEmbedder.java b/shell/platform/android/io/flutter/view/AccessibilityViewEmbedder.java index 2823eb795d17b..33f18ef579c24 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityViewEmbedder.java +++ b/shell/platform/android/io/flutter/view/AccessibilityViewEmbedder.java @@ -9,7 +9,6 @@ import android.os.Build; import android.os.Bundle; import android.os.Parcel; -import android.util.Log; import android.util.SparseArray; import android.view.MotionEvent; import android.view.View; @@ -20,6 +19,7 @@ import androidx.annotation.Keep; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import io.flutter.Log; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -502,9 +502,9 @@ private Long getSourceNodeId(@NonNull AccessibilityNodeInfo node) { try { return (Long) getSourceNodeId.invoke(node); } catch (IllegalAccessException e) { - Log.w(TAG, e); + Log.w(TAG, "Failed to access getSourceNodeId method.", e); } catch (InvocationTargetException e) { - Log.w(TAG, e); + Log.w(TAG, "The getSourceNodeId method threw an exception when invoked.", e); } return null; } @@ -523,9 +523,9 @@ private Long getChildId(@NonNull AccessibilityNodeInfo node, int child) { // type ReflectiveOperationException. As a workaround either create individual // catch statements, or catch Exception. [NewApi] } catch (IllegalAccessException e) { - Log.w(TAG, e); + Log.w(TAG, "Failed to access getChildId method.", e); } catch (InvocationTargetException e) { - Log.w(TAG, e); + Log.w(TAG, "The getChildId method threw an exception when invoked.", e); } } else { try { @@ -536,9 +536,9 @@ private Long getChildId(@NonNull AccessibilityNodeInfo node, int child) { // type ReflectiveOperationException. As a workaround either create individual // catch statements, or catch Exception. [NewApi] } catch (IllegalAccessException e) { - Log.w(TAG, e); + Log.w(TAG, "Failed to access longArrayGetIndex method or the childNodeId field.", e); } catch (InvocationTargetException | ArrayIndexOutOfBoundsException e) { - Log.w(TAG, e); + Log.w(TAG, "The longArrayGetIndex method threw an exception when invoked.", e); } } return null; @@ -555,9 +555,9 @@ private Long getParentNodeId(@NonNull AccessibilityNodeInfo node) { // type ReflectiveOperationException. As a workaround either create individual // catch statements, or catch Exception. [NewApi] } catch (IllegalAccessException e) { - Log.w(TAG, e); + Log.w(TAG, "Failed to access getParentNodeId method.", e); } catch (InvocationTargetException e) { - Log.w(TAG, e); + Log.w(TAG, "The getParentNodeId method threw an exception when invoked.", e); } } @@ -620,9 +620,9 @@ private Long getRecordSourceNodeId(@NonNull AccessibilityRecord node) { try { return (Long) getRecordSourceNodeId.invoke(node); } catch (IllegalAccessException e) { - Log.w(TAG, e); + Log.w(TAG, "Failed to access the getRecordSourceNodeId method.", e); } catch (InvocationTargetException e) { - Log.w(TAG, e); + Log.w(TAG, "The getRecordSourceNodeId method threw an exception when invoked.", e); } return null; } diff --git a/shell/platform/android/io/flutter/view/FlutterNativeView.java b/shell/platform/android/io/flutter/view/FlutterNativeView.java index ea01176ebcec6..2d5ee8c40ad56 100644 --- a/shell/platform/android/io/flutter/view/FlutterNativeView.java +++ b/shell/platform/android/io/flutter/view/FlutterNativeView.java @@ -6,9 +6,9 @@ import android.app.Activity; import android.content.Context; -import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.UiThread; +import io.flutter.Log; import io.flutter.app.FlutterPluginRegistry; import io.flutter.embedding.engine.FlutterEngine.EngineLifecycleListener; import io.flutter.embedding.engine.FlutterJNI; diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index 9899e1714b255..b630ad242a51f 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -19,7 +19,6 @@ import android.os.Handler; import android.text.format.DateFormat; import android.util.AttributeSet; -import android.util.Log; import android.util.SparseArray; import android.view.DisplayCutout; import android.view.KeyEvent; @@ -41,11 +40,13 @@ import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.annotation.UiThread; +import io.flutter.Log; import io.flutter.app.FlutterPluginRegistry; import io.flutter.embedding.android.AndroidKeyProcessor; import io.flutter.embedding.android.AndroidTouchProcessor; import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.renderer.FlutterRenderer; +import io.flutter.embedding.engine.renderer.SurfaceTextureWrapper; import io.flutter.embedding.engine.systemchannels.AccessibilityChannel; import io.flutter.embedding.engine.systemchannels.KeyEventChannel; import io.flutter.embedding.engine.systemchannels.LifecycleChannel; @@ -876,18 +877,18 @@ public TextureRegistry.SurfaceTextureEntry createSurfaceTexture() { surfaceTexture.detachFromGLContext(); final SurfaceTextureRegistryEntry entry = new SurfaceTextureRegistryEntry(nextTextureId.getAndIncrement(), surfaceTexture); - mNativeView.getFlutterJNI().registerTexture(entry.id(), surfaceTexture); + mNativeView.getFlutterJNI().registerTexture(entry.id(), entry.textureWrapper()); return entry; } final class SurfaceTextureRegistryEntry implements TextureRegistry.SurfaceTextureEntry { private final long id; - private final SurfaceTexture surfaceTexture; + private final SurfaceTextureWrapper textureWrapper; private boolean released; SurfaceTextureRegistryEntry(long id, SurfaceTexture surfaceTexture) { this.id = id; - this.surfaceTexture = surfaceTexture; + this.textureWrapper = new SurfaceTextureWrapper(surfaceTexture); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // The callback relies on being executed on the UI thread (unsynchronised read of @@ -895,12 +896,12 @@ final class SurfaceTextureRegistryEntry implements TextureRegistry.SurfaceTextur // and also the engine code check for platform thread in // Shell::OnPlatformViewMarkTextureFrameAvailable), // so we explicitly pass a Handler for the current thread. - this.surfaceTexture.setOnFrameAvailableListener(onFrameListener, new Handler()); + this.surfaceTexture().setOnFrameAvailableListener(onFrameListener, new Handler()); } else { // Android documentation states that the listener can be called on an arbitrary thread. // But in practice, versions of Android that predate the newer API will call the listener // on the thread where the SurfaceTexture was constructed. - this.surfaceTexture.setOnFrameAvailableListener(onFrameListener); + this.surfaceTexture().setOnFrameAvailableListener(onFrameListener); } } @@ -921,9 +922,13 @@ public void onFrameAvailable(SurfaceTexture texture) { } }; + public SurfaceTextureWrapper textureWrapper() { + return textureWrapper; + } + @Override public SurfaceTexture surfaceTexture() { - return surfaceTexture; + return textureWrapper.surfaceTexture(); } @Override @@ -945,8 +950,8 @@ public void release() { // Otherwise onFrameAvailableListener might be called after mNativeView==null // (https://github.com/flutter/flutter/issues/20951). See also the check in onFrameAvailable. - surfaceTexture.setOnFrameAvailableListener(null); - surfaceTexture.release(); + surfaceTexture().setOnFrameAvailableListener(null); + textureWrapper.release(); mNativeView.getFlutterJNI().unregisterTexture(id); } } diff --git a/shell/platform/android/platform_view_android.cc b/shell/platform/android/platform_view_android.cc index 2c887b638a060..8ebac2aa929b9 100644 --- a/shell/platform/android/platform_view_android.cc +++ b/shell/platform/android/platform_view_android.cc @@ -14,6 +14,8 @@ #include "flutter/shell/platform/android/android_external_texture_gl.h" #include "flutter/shell/platform/android/android_surface_gl.h" #include "flutter/shell/platform/android/android_surface_software.h" +#include "flutter/shell/platform/android/external_view_embedder/external_view_embedder.h" +#include "flutter/shell/platform/android/surface/android_surface.h" #if SHELL_ENABLE_VULKAN #include "flutter/shell/platform/android/android_surface_vulkan.h" @@ -26,22 +28,35 @@ namespace flutter { -std::unique_ptr SurfaceFactory( - std::shared_ptr android_context, - std::shared_ptr jni_facade) { - FML_CHECK(SurfaceFactory); - switch (android_context->RenderingApi()) { +AndroidSurfaceFactoryImpl::AndroidSurfaceFactoryImpl( + std::shared_ptr context, + std::shared_ptr jni_facade) + : android_context_(context), jni_facade_(jni_facade) {} + +AndroidSurfaceFactoryImpl::~AndroidSurfaceFactoryImpl() = default; + +void AndroidSurfaceFactoryImpl::SetExternalViewEmbedder( + std::shared_ptr external_view_embedder) { + external_view_embedder_ = external_view_embedder; +} + +std::unique_ptr AndroidSurfaceFactoryImpl::CreateSurface() { + std::shared_ptr external_view_embedder = + external_view_embedder_.lock(); + FML_CHECK(external_view_embedder); + switch (android_context_->RenderingApi()) { case AndroidRenderingAPI::kSoftware: return std::make_unique( - android_context, jni_facade, SurfaceFactory); + android_context_, jni_facade_, external_view_embedder); case AndroidRenderingAPI::kOpenGLES: - return std::make_unique(android_context, jni_facade, - SurfaceFactory); + return std::make_unique(android_context_, jni_facade_, + external_view_embedder); case AndroidRenderingAPI::kVulkan: #if SHELL_ENABLE_VULKAN - return std::make_unique(android_context, jni_facade, - SurfaceFactory); + return std::make_unique( + android_context_, jni_facade_, external_view_embedder); #endif // SHELL_ENABLE_VULKAN + default: return nullptr; } return nullptr; @@ -72,7 +87,13 @@ PlatformViewAndroid::PlatformViewAndroid( FML_CHECK(android_context && android_context->IsValid()) << "Could not create an Android context."; - android_surface_ = SurfaceFactory(std::move(android_context), jni_facade); + surface_factory_ = + std::make_shared(android_context, jni_facade); + external_view_embedder_ = std::make_shared( + android_context, jni_facade, surface_factory_); + surface_factory_->SetExternalViewEmbedder(external_view_embedder_); + + android_surface_ = surface_factory_->CreateSurface(); FML_CHECK(android_surface_ && android_surface_->IsValid()) << "Could not create an OpenGL, Vulkan or Software surface to setup " "rendering."; diff --git a/shell/platform/android/platform_view_android.h b/shell/platform/android/platform_view_android.h index 618b0413a572f..b1bf8194976f5 100644 --- a/shell/platform/android/platform_view_android.h +++ b/shell/platform/android/platform_view_android.h @@ -22,6 +22,24 @@ namespace flutter { +class AndroidSurfaceFactoryImpl : public AndroidSurfaceFactory { + public: + AndroidSurfaceFactoryImpl(std::shared_ptr context, + std::shared_ptr jni_facade); + + ~AndroidSurfaceFactoryImpl() override; + + std::unique_ptr CreateSurface() override; + + void SetExternalViewEmbedder( + std::shared_ptr external_view_embedder); + + private: + std::shared_ptr android_context_; + std::shared_ptr jni_facade_; + std::weak_ptr external_view_embedder_; +}; + class PlatformViewAndroid final : public PlatformView { public: static bool Register(JNIEnv* env); @@ -80,6 +98,8 @@ class PlatformViewAndroid final : public PlatformView { private: const std::shared_ptr jni_facade_; + std::shared_ptr external_view_embedder_; + std::shared_ptr surface_factory_; PlatformViewAndroidDelegate platform_view_android_delegate_; diff --git a/shell/platform/android/platform_view_android_jni_impl.cc b/shell/platform/android/platform_view_android_jni_impl.cc index 3b8a61bf39c0c..b3b11dbb8ee3e 100644 --- a/shell/platform/android/platform_view_android_jni_impl.cc +++ b/shell/platform/android/platform_view_android_jni_impl.cc @@ -53,7 +53,7 @@ static fml::jni::ScopedJavaGlobalRef* g_flutter_callback_info_class = static fml::jni::ScopedJavaGlobalRef* g_flutter_jni_class = nullptr; -static fml::jni::ScopedJavaGlobalRef* g_surface_texture_class = nullptr; +static fml::jni::ScopedJavaGlobalRef* g_texture_wrapper_class = nullptr; // Called By Native @@ -613,7 +613,8 @@ bool RegisterApi(JNIEnv* env) { }, { .name = "nativeRegisterTexture", - .signature = "(JJLandroid/graphics/SurfaceTexture;)V", + .signature = "(JJLio/flutter/embedding/engine/renderer/" + "SurfaceTextureWrapper;)V", .fnPtr = reinterpret_cast(&RegisterTexture), }, { @@ -857,15 +858,16 @@ bool PlatformViewAndroid::Register(JNIEnv* env) { return false; } - g_surface_texture_class = new fml::jni::ScopedJavaGlobalRef( - env, env->FindClass("android/graphics/SurfaceTexture")); - if (g_surface_texture_class->is_null()) { - FML_LOG(ERROR) << "Could not locate SurfaceTexture class"; + g_texture_wrapper_class = new fml::jni::ScopedJavaGlobalRef( + env, env->FindClass( + "io/flutter/embedding/engine/renderer/SurfaceTextureWrapper")); + if (g_texture_wrapper_class->is_null()) { + FML_LOG(ERROR) << "Could not locate SurfaceTextureWrapper class"; return false; } g_attach_to_gl_context_method = env->GetMethodID( - g_surface_texture_class->obj(), "attachToGLContext", "(I)V"); + g_texture_wrapper_class->obj(), "attachToGLContext", "(I)V"); if (g_attach_to_gl_context_method == nullptr) { FML_LOG(ERROR) << "Could not locate attachToGlContext method"; @@ -873,7 +875,7 @@ bool PlatformViewAndroid::Register(JNIEnv* env) { } g_update_tex_image_method = - env->GetMethodID(g_surface_texture_class->obj(), "updateTexImage", "()V"); + env->GetMethodID(g_texture_wrapper_class->obj(), "updateTexImage", "()V"); if (g_update_tex_image_method == nullptr) { FML_LOG(ERROR) << "Could not locate updateTexImage method"; @@ -881,7 +883,7 @@ bool PlatformViewAndroid::Register(JNIEnv* env) { } g_get_transform_matrix_method = env->GetMethodID( - g_surface_texture_class->obj(), "getTransformMatrix", "([F)V"); + g_texture_wrapper_class->obj(), "getTransformMatrix", "([F)V"); if (g_get_transform_matrix_method == nullptr) { FML_LOG(ERROR) << "Could not locate getTransformMatrix method"; @@ -889,7 +891,7 @@ bool PlatformViewAndroid::Register(JNIEnv* env) { } g_detach_from_gl_context_method = env->GetMethodID( - g_surface_texture_class->obj(), "detachFromGLContext", "()V"); + g_texture_wrapper_class->obj(), "detachFromGLContext", "()V"); if (g_detach_from_gl_context_method == nullptr) { FML_LOG(ERROR) << "Could not locate detachFromGlContext method"; diff --git a/shell/platform/android/surface/android_surface.h b/shell/platform/android/surface/android_surface.h index 98c4f629414ae..1fd4847fae27b 100644 --- a/shell/platform/android/surface/android_surface.h +++ b/shell/platform/android/surface/android_surface.h @@ -5,6 +5,7 @@ #ifndef FLUTTER_SHELL_PLATFORM_ANDROID_ANDROID_SURFACE_H_ #define FLUTTER_SHELL_PLATFORM_ANDROID_ANDROID_SURFACE_H_ +#include "flutter/flow/embedded_views.h" #include "flutter/flow/surface.h" #include "flutter/fml/macros.h" #include "flutter/shell/platform/android/context/android_context.h" @@ -14,12 +15,10 @@ namespace flutter { +class AndroidExternalViewEmbedder; + class AndroidSurface { public: - using Factory = std::function( - std::shared_ptr android_context, - std::shared_ptr jni_facade)>; - virtual ~AndroidSurface(); virtual bool IsValid() const = 0; @@ -38,6 +37,15 @@ class AndroidSurface { virtual bool SetNativeWindow(fml::RefPtr window) = 0; }; +class AndroidSurfaceFactory { + public: + AndroidSurfaceFactory() = default; + + virtual ~AndroidSurfaceFactory() = default; + + virtual std::unique_ptr CreateSurface() = 0; +}; + } // namespace flutter #endif // FLUTTER_SHELL_PLATFORM_ANDROID_ANDROID_SURFACE_H_ diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java index c96f8d5dd5770..f440f736589e1 100644 --- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java +++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java @@ -16,6 +16,8 @@ import io.flutter.embedding.engine.FlutterJNITest; import io.flutter.embedding.engine.LocalizationPluginTest; import io.flutter.embedding.engine.RenderingComponentTest; +import io.flutter.embedding.engine.dart.DartExecutorTest; +import io.flutter.embedding.engine.dart.DartMessengerTest; import io.flutter.embedding.engine.loader.ApplicationInfoLoaderTest; import io.flutter.embedding.engine.loader.FlutterLoaderTest; import io.flutter.embedding.engine.mutatorsstack.FlutterMutatorViewTest; @@ -41,7 +43,6 @@ import test.io.flutter.embedding.engine.FlutterEngineTest; import test.io.flutter.embedding.engine.FlutterShellArgsTest; import test.io.flutter.embedding.engine.PluginComponentTest; -import test.io.flutter.embedding.engine.dart.DartExecutorTest; @RunWith(Suite.class) @SuiteClasses({ @@ -49,6 +50,7 @@ AndroidKeyProcessorTest.class, ApplicationInfoLoaderTest.class, DartExecutorTest.class, + DartMessengerTest.class, FlutterActivityAndFragmentDelegateTest.class, FlutterActivityTest.class, FlutterAndroidComponentTest.class, diff --git a/shell/platform/android/test/io/flutter/embedding/engine/dart/DartExecutorTest.java b/shell/platform/android/test/io/flutter/embedding/engine/dart/DartExecutorTest.java index 6c8e766bd557b..51a341ce45f04 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/dart/DartExecutorTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/dart/DartExecutorTest.java @@ -1,4 +1,4 @@ -package test.io.flutter.embedding.engine.dart; +package io.flutter.embedding.engine.dart; import static junit.framework.TestCase.assertNotNull; import static org.junit.Assert.assertEquals; diff --git a/shell/platform/android/test/io/flutter/embedding/engine/dart/DartMessengerTest.java b/shell/platform/android/test/io/flutter/embedding/engine/dart/DartMessengerTest.java new file mode 100644 index 0000000000000..1665047cad861 --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/engine/dart/DartMessengerTest.java @@ -0,0 +1,55 @@ +package io.flutter.embedding.engine.dart; + +import static junit.framework.TestCase.assertNotNull; +import static junit.framework.TestCase.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; + +import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.plugin.common.BinaryMessenger.BinaryMessageHandler; +import java.nio.ByteBuffer; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@Config(manifest = Config.NONE) +@RunWith(RobolectricTestRunner.class) +public class DartMessengerTest { + private static class ReportingUncaughtExceptionHandler + implements Thread.UncaughtExceptionHandler { + public Throwable latestException; + + @Override + public void uncaughtException(Thread t, Throwable e) { + latestException = e; + } + } + + @Test + public void itHandlesErrors() { + // Setup test. + final FlutterJNI fakeFlutterJni = mock(FlutterJNI.class); + final Thread currentThread = Thread.currentThread(); + final Thread.UncaughtExceptionHandler savedHandler = + currentThread.getUncaughtExceptionHandler(); + final ReportingUncaughtExceptionHandler reportingHandler = + new ReportingUncaughtExceptionHandler(); + currentThread.setUncaughtExceptionHandler(reportingHandler); + + // Create object under test. + final DartMessenger messenger = new DartMessenger(fakeFlutterJni); + final BinaryMessageHandler throwingHandler = mock(BinaryMessageHandler.class); + Mockito.doThrow(AssertionError.class) + .when(throwingHandler) + .onMessage(any(ByteBuffer.class), any(DartMessenger.Reply.class)); + + messenger.setMessageHandler("test", throwingHandler); + messenger.handleMessageFromDart("test", new byte[] {}, 0); + assertNotNull(reportingHandler.latestException); + assertTrue(reportingHandler.latestException instanceof AssertionError); + currentThread.setUncaughtExceptionHandler(savedHandler); + } +} + diff --git a/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java index 495b332678fe9..0d9e0ea88e16c 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java @@ -1,5 +1,6 @@ package io.flutter.plugin.platform; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -10,11 +11,17 @@ import android.app.Activity; import android.content.ClipData; import android.content.ClipboardManager; +import android.content.ContentResolver; import android.content.Context; +import android.media.RingtoneManager; +import android.net.Uri; import android.view.View; import android.view.Window; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.embedding.engine.systemchannels.PlatformChannel.ClipboardContentFormat; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -42,8 +49,9 @@ public void itIgnoresNewHapticEventsOnOldAndroidPlatforms() { platformPlugin.vibrateHapticFeedback(PlatformChannel.HapticFeedbackType.SELECTION_CLICK); } + @Config(sdk = 29) @Test - public void platformPlugin_getClipboardData() { + public void platformPlugin_getClipboardData() throws IOException { ClipboardManager clipboardManager = RuntimeEnvironment.application.getSystemService(ClipboardManager.class); @@ -61,6 +69,23 @@ public void platformPlugin_getClipboardData() { ClipData clip = ClipData.newPlainText("label", "Text"); clipboardManager.setPrimaryClip(clip); assertNotNull(platformPlugin.mPlatformMessageHandler.getClipboardData(clipboardFormat)); + + ContentResolver contentResolver = RuntimeEnvironment.application.getContentResolver(); + Uri uri = Uri.parse("content://media/external_primary/images/media/"); + clip = ClipData.newUri(contentResolver, "URI", uri); + clipboardManager.setPrimaryClip(clip); + assertNull(platformPlugin.mPlatformMessageHandler.getClipboardData(clipboardFormat)); + + uri = + RingtoneManager.getActualDefaultRingtoneUri( + RuntimeEnvironment.application.getApplicationContext(), RingtoneManager.TYPE_RINGTONE); + clip = ClipData.newUri(contentResolver, "URI", uri); + clipboardManager.setPrimaryClip(clip); + String uriData = + platformPlugin.mPlatformMessageHandler.getClipboardData(clipboardFormat).toString(); + InputStream uriInputStream = contentResolver.openInputStream(uri); + InputStream dataInputStream = new ByteArrayInputStream(uriData.getBytes()); + assertEquals(dataInputStream.read(), uriInputStream.read()); } @Test diff --git a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java index 0e585b3e508c4..62806032cd092 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java @@ -29,9 +29,7 @@ import io.flutter.embedding.engine.systemchannels.MouseCursorChannel; import io.flutter.embedding.engine.systemchannels.SettingsChannel; import io.flutter.embedding.engine.systemchannels.TextInputChannel; -import io.flutter.plugin.common.FlutterException; import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.StandardMessageCodec; import io.flutter.plugin.common.StandardMethodCodec; import io.flutter.plugin.localization.LocalizationPlugin; import java.nio.ByteBuffer; @@ -262,7 +260,7 @@ public void createPlatformViewMessage__initializesAndroidView() { // Simulate create call from the framework. createPlatformView(jni, platformViewsController, platformViewId, "testType"); - verify(platformView, times(1)).getView(); + verify(viewFactory, times(1)).create(any(), eq(platformViewId), any()); } @Test @@ -286,21 +284,11 @@ public void createPlatformViewMessage__throwsIfViewIsNull() { createPlatformView(jni, platformViewsController, platformViewId, "testType"); assertEquals(ShadowFlutterJNI.getResponses().size(), 1); - final ByteBuffer responseBuffer = ShadowFlutterJNI.getResponses().get(0); - responseBuffer.rewind(); - - StandardMethodCodec methodCodec = new StandardMethodCodec(new StandardMessageCodec()); - try { - methodCodec.decodeEnvelope(responseBuffer); - } catch (FlutterException exception) { - assertTrue( - exception - .getMessage() - .contains( - "PlatformView#getView() returned null, but an Android view reference was expected.")); - return; - } - assertFalse(true); + assertThrows( + IllegalStateException.class, + () -> { + platformViewsController.initializePlatformViewIfNeeded(platformViewId); + }); } @Test @@ -326,21 +314,11 @@ public void createPlatformViewMessage__throwsIfViewHasParent() { createPlatformView(jni, platformViewsController, platformViewId, "testType"); assertEquals(ShadowFlutterJNI.getResponses().size(), 1); - final ByteBuffer responseBuffer = ShadowFlutterJNI.getResponses().get(0); - responseBuffer.rewind(); - - StandardMethodCodec methodCodec = new StandardMethodCodec(new StandardMessageCodec()); - try { - methodCodec.decodeEnvelope(responseBuffer); - } catch (FlutterException exception) { - assertTrue( - exception - .getMessage() - .contains( - "The Android view returned from PlatformView#getView() was already added to a parent view.")); - return; - } - assertFalse(true); + assertThrows( + IllegalStateException.class, + () -> { + platformViewsController.initializePlatformViewIfNeeded(platformViewId); + }); } @Test @@ -381,6 +359,7 @@ public void disposeAndroidView__hybridComposition() { assertNotNull(androidView.getParent()); assertTrue(androidView.getParent() instanceof FlutterMutatorView); + verify(platformView, times(1)).dispose(); } @Test diff --git a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java index 81d87ae56afa8..673c1e48c95eb 100644 --- a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java +++ b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java @@ -5,6 +5,7 @@ package io.flutter.view; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; @@ -16,14 +17,17 @@ import static org.mockito.Mockito.when; import android.annotation.TargetApi; +import android.app.Activity; import android.content.ContentResolver; import android.content.Context; +import android.graphics.Rect; import android.view.MotionEvent; import android.view.View; import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; +import io.flutter.embedding.android.FlutterActivity; import io.flutter.embedding.engine.systemchannels.AccessibilityChannel; import io.flutter.plugin.platform.PlatformViewsAccessibilityDelegate; import java.nio.ByteBuffer; @@ -33,6 +37,7 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @Config(manifest = Config.NONE) @@ -198,6 +203,81 @@ public void itHoverOverOutOfBoundsDoesNotCrash() { accessibilityBridge.onAccessibilityHoverEvent(MotionEvent.obtain(1, 1, 1, -10, -10, 0)); } + @Test + public void itProducesPlatformViewNodeForHybridComposition() { + PlatformViewsAccessibilityDelegate accessibilityDelegate = + mock(PlatformViewsAccessibilityDelegate.class); + + Context context = RuntimeEnvironment.application.getApplicationContext(); + View rootAccessibilityView = new View(context); + AccessibilityViewEmbedder accessibilityViewEmbedder = mock(AccessibilityViewEmbedder.class); + AccessibilityBridge accessibilityBridge = + setUpBridge( + rootAccessibilityView, + /*accessibilityChannel=*/ null, + /*accessibilityManager=*/ null, + /*contentResolver=*/ null, + accessibilityViewEmbedder, + accessibilityDelegate); + + TestSemanticsNode root = new TestSemanticsNode(); + root.id = 0; + + TestSemanticsNode platformView = new TestSemanticsNode(); + platformView.id = 1; + platformView.platformViewId = 1; + root.addChild(platformView); + + TestSemanticsUpdate testSemanticsRootUpdate = root.toUpdate(); + accessibilityBridge.updateSemantics( + testSemanticsRootUpdate.buffer, testSemanticsRootUpdate.strings); + + TestSemanticsUpdate testSemanticsPlatformViewUpdate = platformView.toUpdate(); + accessibilityBridge.updateSemantics( + testSemanticsPlatformViewUpdate.buffer, testSemanticsPlatformViewUpdate.strings); + + View embeddedView = mock(View.class); + when(accessibilityDelegate.getPlatformViewById(1)).thenReturn(embeddedView); + + when(embeddedView.getContext()).thenReturn(mock(FlutterActivity.class)); + + AccessibilityNodeInfo nodeInfo = mock(AccessibilityNodeInfo.class); + when(embeddedView.createAccessibilityNodeInfo()).thenReturn(nodeInfo); + + AccessibilityNodeInfo result = accessibilityBridge.createAccessibilityNodeInfo(0); + assertNotNull(result); + assertEquals(result.getChildCount(), 1); + assertEquals(result.getClassName(), "android.view.View"); + } + + @Test + public void itProducesPlatformViewNodeForVirtualDisplay() { + PlatformViewsAccessibilityDelegate accessibilityDelegate = + mock(PlatformViewsAccessibilityDelegate.class); + AccessibilityViewEmbedder accessibilityViewEmbedder = mock(AccessibilityViewEmbedder.class); + AccessibilityBridge accessibilityBridge = + setUpBridge( + /*rootAccessibilityView=*/ null, + /*accessibilityChannel=*/ null, + /*accessibilityManager=*/ null, + /*contentResolver=*/ null, + accessibilityViewEmbedder, + accessibilityDelegate); + + TestSemanticsNode platformView = new TestSemanticsNode(); + platformView.platformViewId = 1; + + TestSemanticsUpdate testSemanticsUpdate = platformView.toUpdate(); + accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings); + + View embeddedView = mock(View.class); + when(accessibilityDelegate.getPlatformViewById(1)).thenReturn(embeddedView); + when(embeddedView.getContext()).thenReturn(mock(Activity.class)); + + accessibilityBridge.createAccessibilityNodeInfo(0); + verify(accessibilityViewEmbedder).getRootNode(eq(embeddedView), eq(0), any(Rect.class)); + } + @Test public void releaseDropsChannelMessageHandler() { AccessibilityChannel mockChannel = mock(AccessibilityChannel.class); @@ -317,6 +397,10 @@ void addFlag(AccessibilityBridge.Flag flag) { float right = 0.0f; float bottom = 0.0f; final List children = new ArrayList(); + + public void addChild(TestSemanticsNode child) { + children.add(child); + } // custom actions not supported. TestSemanticsUpdate toUpdate() { diff --git a/shell/platform/common/cpp/BUILD.gn b/shell/platform/common/cpp/BUILD.gn index a25c38f652588..d1b4888c2f9e9 100644 --- a/shell/platform/common/cpp/BUILD.gn +++ b/shell/platform/common/cpp/BUILD.gn @@ -37,7 +37,10 @@ copy("publish_headers") { } source_set("common_cpp_input") { - public = [ "text_input_model.h" ] + public = [ + "text_input_model.h", + "text_range.h", + ] sources = [ "text_input_model.cc" ] @@ -45,6 +48,8 @@ source_set("common_cpp_input") { public_configs = [ "//flutter:config" ] + deps = [ "//flutter/fml:fml" ] + if (is_win) { # For wstring_conversion. See issue #50053. defines = [ "_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING" ] @@ -137,6 +142,7 @@ if (enable_unittests) { "json_message_codec_unittests.cc", "json_method_codec_unittests.cc", "text_input_model_unittests.cc", + "text_range_unittests.cc", ] deps = [ diff --git a/shell/platform/common/cpp/client_wrapper/encodable_value_unittests.cc b/shell/platform/common/cpp/client_wrapper/encodable_value_unittests.cc index 45a62183127de..1f31c47b67c84 100644 --- a/shell/platform/common/cpp/client_wrapper/encodable_value_unittests.cc +++ b/shell/platform/common/cpp/client_wrapper/encodable_value_unittests.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/encodable_value.h" diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registrar.h b/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registrar.h index 06a34e0bf9a29..a01b75e8c9136 100644 --- a/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registrar.h +++ b/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registrar.h @@ -51,6 +51,11 @@ class PluginRegistrar { protected: FlutterDesktopPluginRegistrarRef registrar() { return registrar_; } + // Destroys all owned plugins. Subclasses should call this at the beginning of + // their destructors to prevent the possibility of an owned plugin trying to + // access destroyed state during its own destruction. + void ClearPlugins(); + private: // Handle for interacting with the C API's registrar. FlutterDesktopPluginRegistrarRef registrar_; diff --git a/shell/platform/common/cpp/client_wrapper/plugin_registrar.cc b/shell/platform/common/cpp/client_wrapper/plugin_registrar.cc index b81411fb40e01..b5ab707c80aec 100644 --- a/shell/platform/common/cpp/client_wrapper/plugin_registrar.cc +++ b/shell/platform/common/cpp/client_wrapper/plugin_registrar.cc @@ -21,12 +21,22 @@ PluginRegistrar::PluginRegistrar(FlutterDesktopPluginRegistrarRef registrar) messenger_ = std::make_unique(core_messenger); } -PluginRegistrar::~PluginRegistrar() {} +PluginRegistrar::~PluginRegistrar() { + // This must always be the first call. + ClearPlugins(); + + // Explicitly cleared to facilitate testing of destruction order. + messenger_.reset(); +} void PluginRegistrar::AddPlugin(std::unique_ptr plugin) { plugins_.insert(std::move(plugin)); } +void PluginRegistrar::ClearPlugins() { + plugins_.clear(); +} + // ===== PluginRegistrarManager ===== // static diff --git a/shell/platform/common/cpp/client_wrapper/plugin_registrar_unittests.cc b/shell/platform/common/cpp/client_wrapper/plugin_registrar_unittests.cc index 739b959a8814b..b3a92aa67d714 100644 --- a/shell/platform/common/cpp/client_wrapper/plugin_registrar_unittests.cc +++ b/shell/platform/common/cpp/client_wrapper/plugin_registrar_unittests.cc @@ -80,8 +80,41 @@ class TestPluginRegistrar : public PluginRegistrar { std::function destruction_callback_; }; +// A test plugin that tries to access registrar state during destruction and +// reports it out via a flag provided at construction. +class TestPlugin : public Plugin { + public: + // registrar_valid_at_destruction will be set at destruction to indicate + // whether or not |registrar->messenger()| was non-null. + TestPlugin(PluginRegistrar* registrar, bool* registrar_valid_at_destruction) + : registrar_(registrar), + registrar_valid_at_destruction_(registrar_valid_at_destruction) {} + virtual ~TestPlugin() { + *registrar_valid_at_destruction_ = registrar_->messenger() != nullptr; + } + + private: + PluginRegistrar* registrar_; + bool* registrar_valid_at_destruction_; +}; + } // namespace +// Tests that the registrar runs plugin destructors before its own teardown. +TEST(PluginRegistrarTest, PluginDestroyedBeforeRegistrar) { + auto dummy_registrar_handle = + reinterpret_cast(1); + bool registrar_valid_at_destruction = false; + { + PluginRegistrar registrar(dummy_registrar_handle); + + auto plugin = std::make_unique(®istrar, + ®istrar_valid_at_destruction); + registrar.AddPlugin(std::move(plugin)); + } + EXPECT_TRUE(registrar_valid_at_destruction); +} + // Tests that the registrar returns a messenger that passes Send through to the // C API. TEST(PluginRegistrarTest, MessengerSend) { diff --git a/shell/platform/common/cpp/client_wrapper/standard_codec.cc b/shell/platform/common/cpp/client_wrapper/standard_codec.cc index db2f2422e519d..3e86879b4775a 100644 --- a/shell/platform/common/cpp/client_wrapper/standard_codec.cc +++ b/shell/platform/common/cpp/client_wrapper/standard_codec.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT // This file contains what would normally be standard_codec_serializer.cc, // standard_message_codec.cc, and standard_method_codec.cc. They are grouped diff --git a/shell/platform/common/cpp/json_message_codec.cc b/shell/platform/common/cpp/json_message_codec.cc index 6d89c6acfeb41..d2a43bd806ded 100644 --- a/shell/platform/common/cpp/json_message_codec.cc +++ b/shell/platform/common/cpp/json_message_codec.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #include "flutter/shell/platform/common/cpp/json_message_codec.h" diff --git a/shell/platform/common/cpp/json_message_codec_unittests.cc b/shell/platform/common/cpp/json_message_codec_unittests.cc index 74dca356a56c7..3354f30a728df 100644 --- a/shell/platform/common/cpp/json_message_codec_unittests.cc +++ b/shell/platform/common/cpp/json_message_codec_unittests.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #include "flutter/shell/platform/common/cpp/json_message_codec.h" diff --git a/shell/platform/common/cpp/json_method_codec.cc b/shell/platform/common/cpp/json_method_codec.cc index f026061ad235f..f7e4428844eba 100644 --- a/shell/platform/common/cpp/json_method_codec.cc +++ b/shell/platform/common/cpp/json_method_codec.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #include "flutter/shell/platform/common/cpp/json_method_codec.h" diff --git a/shell/platform/common/cpp/json_method_codec_unittests.cc b/shell/platform/common/cpp/json_method_codec_unittests.cc index da1046425702c..f9d2ec882fc45 100644 --- a/shell/platform/common/cpp/json_method_codec_unittests.cc +++ b/shell/platform/common/cpp/json_method_codec_unittests.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #include "flutter/shell/platform/common/cpp/json_method_codec.h" diff --git a/shell/platform/common/cpp/text_input_model.cc b/shell/platform/common/cpp/text_input_model.cc index 17f3beb01d84c..649baede20f51 100644 --- a/shell/platform/common/cpp/text_input_model.cc +++ b/shell/platform/common/cpp/text_input_model.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #include "flutter/shell/platform/common/cpp/text_input_model.h" @@ -30,8 +29,7 @@ bool IsTrailingSurrogate(char32_t code_point) { } // namespace -TextInputModel::TextInputModel() - : selection_base_(text_.begin()), selection_extent_(text_.begin()) {} +TextInputModel::TextInputModel() = default; TextInputModel::~TextInputModel() = default; @@ -39,22 +37,77 @@ void TextInputModel::SetText(const std::string& text) { std::wstring_convert, char16_t> utf16_converter; text_ = utf16_converter.from_bytes(text); - selection_base_ = text_.begin(); - selection_extent_ = selection_base_; + selection_ = TextRange(0); + composing_range_ = TextRange(0); } -bool TextInputModel::SetSelection(size_t base, size_t extent) { - if (base > text_.size() || extent > text_.size()) { +bool TextInputModel::SetSelection(const TextRange& range) { + if (composing_ && !range.collapsed()) { return false; } - selection_base_ = text_.begin() + base; - selection_extent_ = text_.begin() + extent; + if (!editable_range().Contains(range)) { + return false; + } + selection_ = range; return true; } -void TextInputModel::DeleteSelected() { - selection_base_ = text_.erase(selection_start(), selection_end()); - selection_extent_ = selection_base_; +bool TextInputModel::SetComposingRange(const TextRange& range, + size_t cursor_offset) { + if (!composing_ || !text_range().Contains(range)) { + return false; + } + composing_range_ = range; + selection_ = TextRange(range.start() + cursor_offset); + return true; +} + +void TextInputModel::BeginComposing() { + composing_ = true; + composing_range_ = TextRange(selection_.start()); +} + +void TextInputModel::UpdateComposingText(const std::string& composing_text) { + std::wstring_convert, char16_t> + utf16_converter; + std::u16string text = utf16_converter.from_bytes(composing_text); + + // Preserve selection if we get a no-op update to the composing region. + if (text.length() == 0 && composing_range_.collapsed()) { + return; + } + DeleteSelected(); + text_.replace(composing_range_.start(), composing_range_.length(), text); + composing_range_.set_end(composing_range_.start() + text.length()); + selection_ = TextRange(composing_range_.end()); +} + +void TextInputModel::CommitComposing() { + // Preserve selection if no composing text was entered. + if (composing_range_.collapsed()) { + return; + } + composing_range_ = TextRange(composing_range_.end()); + selection_ = composing_range_; +} + +void TextInputModel::EndComposing() { + composing_ = false; + composing_range_ = TextRange(0); +} + +bool TextInputModel::DeleteSelected() { + if (selection_.collapsed()) { + return false; + } + size_t start = selection_.start(); + text_.erase(start, selection_.length()); + selection_ = TextRange(start); + if (composing_) { + // This occurs only immediately after composing has begun with a selection. + composing_range_ = selection_; + } + return true; } void TextInputModel::AddCodePoint(char32_t c) { @@ -72,12 +125,16 @@ void TextInputModel::AddCodePoint(char32_t c) { } void TextInputModel::AddText(const std::u16string& text) { - if (selection_base_ != selection_extent_) { - DeleteSelected(); + DeleteSelected(); + if (composing_) { + // Delete the current composing text, set the cursor to composing start. + text_.erase(composing_range_.start(), composing_range_.length()); + selection_ = TextRange(composing_range_.start()); + composing_range_.set_end(composing_range_.start() + text.length()); } - selection_extent_ = text_.insert(selection_extent_, text.begin(), text.end()); - selection_extent_ += text.length(); - selection_base_ = selection_extent_; + size_t position = selection_.position(); + text_.insert(position, text); + selection_ = TextRange(position + text.length()); } void TextInputModel::AddText(const std::string& text) { @@ -87,123 +144,126 @@ void TextInputModel::AddText(const std::string& text) { } bool TextInputModel::Backspace() { - if (selection_base_ != selection_extent_) { - DeleteSelected(); + if (DeleteSelected()) { return true; } - if (selection_base_ != text_.begin()) { - int count = IsTrailingSurrogate(*(selection_base_ - 1)) ? 2 : 1; - selection_base_ = text_.erase(selection_base_ - count, selection_base_); - selection_extent_ = selection_base_; + // There is no selection. Delete the preceding codepoint. + size_t position = selection_.position(); + if (position != editable_range().start()) { + int count = IsTrailingSurrogate(text_.at(position - 1)) ? 2 : 1; + text_.erase(position - count, count); + selection_ = TextRange(position - count); + if (composing_) { + composing_range_.set_end(composing_range_.end() - count); + } return true; } - return false; // No edits happened. + return false; } bool TextInputModel::Delete() { - if (selection_base_ != selection_extent_) { - DeleteSelected(); + if (DeleteSelected()) { return true; } - if (selection_base_ != text_.end()) { - int count = IsLeadingSurrogate(*selection_base_) ? 2 : 1; - selection_base_ = text_.erase(selection_base_, selection_base_ + count); - selection_extent_ = selection_base_; + // There is no selection. Delete the preceding codepoint. + size_t position = selection_.position(); + if (position < editable_range().end()) { + int count = IsLeadingSurrogate(text_.at(position)) ? 2 : 1; + text_.erase(position, count); + if (composing_) { + composing_range_.set_end(composing_range_.end() - count); + } return true; } return false; } bool TextInputModel::DeleteSurrounding(int offset_from_cursor, int count) { - auto start = selection_extent_; + size_t max_pos = editable_range().end(); + size_t start = selection_.extent(); if (offset_from_cursor < 0) { for (int i = 0; i < -offset_from_cursor; i++) { // If requested start is before the available text then reduce the // number of characters to delete. - if (start == text_.begin()) { + if (start == editable_range().start()) { count = i; break; } - start -= IsTrailingSurrogate(*(start - 1)) ? 2 : 1; + start -= IsTrailingSurrogate(text_.at(start - 1)) ? 2 : 1; } } else { - for (int i = 0; i < offset_from_cursor && start != text_.end(); i++) { - start += IsLeadingSurrogate(*start) ? 2 : 1; + for (int i = 0; i < offset_from_cursor && start != max_pos; i++) { + start += IsLeadingSurrogate(text_.at(start)) ? 2 : 1; } } auto end = start; - for (int i = 0; i < count && end != text_.end(); i++) { - end += IsLeadingSurrogate(*start) ? 2 : 1; + for (int i = 0; i < count && end != max_pos; i++) { + end += IsLeadingSurrogate(text_.at(start)) ? 2 : 1; } if (start == end) { return false; } - auto new_base = text_.erase(start, end); + auto deleted_length = end - start; + text_.erase(start, deleted_length); // Cursor moves only if deleted area is before it. - if (offset_from_cursor <= 0) { - selection_base_ = new_base; - } - - // Clear selection. - selection_extent_ = selection_base_; + selection_ = TextRange(offset_from_cursor <= 0 ? start : selection_.start()); + // Adjust composing range. + if (composing_) { + composing_range_.set_end(composing_range_.end() - deleted_length); + } return true; } bool TextInputModel::MoveCursorToBeginning() { - if (selection_base_ == text_.begin() && selection_extent_ == text_.begin()) + size_t min_pos = editable_range().start(); + if (selection_.collapsed() && selection_.position() == min_pos) { return false; - - selection_base_ = text_.begin(); - selection_extent_ = text_.begin(); - + } + selection_ = TextRange(min_pos); return true; } bool TextInputModel::MoveCursorToEnd() { - if (selection_base_ == text_.end() && selection_extent_ == text_.end()) + size_t max_pos = editable_range().end(); + if (selection_.collapsed() && selection_.position() == max_pos) { return false; - - selection_base_ = text_.end(); - selection_extent_ = text_.end(); - + } + selection_ = TextRange(max_pos); return true; } bool TextInputModel::MoveCursorForward() { - // If about to move set to the end of the highlight (when not selecting). - if (selection_base_ != selection_extent_) { - selection_base_ = selection_end(); - selection_extent_ = selection_base_; + // If there's a selection, move to the end of the selection. + if (!selection_.collapsed()) { + selection_ = TextRange(selection_.end()); return true; } - // If not at the end, move the extent forward. - if (selection_extent_ != text_.end()) { - int count = IsLeadingSurrogate(*selection_base_) ? 2 : 1; - selection_base_ += count; - selection_extent_ = selection_base_; + // Otherwise, move the cursor forward. + size_t position = selection_.position(); + if (position != editable_range().end()) { + int count = IsLeadingSurrogate(text_.at(position)) ? 2 : 1; + selection_ = TextRange(position + count); return true; } return false; } bool TextInputModel::MoveCursorBack() { - // If about to move set to the beginning of the highlight - // (when not selecting). - if (selection_base_ != selection_extent_) { - selection_base_ = selection_start(); - selection_extent_ = selection_base_; + // If there's a selection, move to the beginning of the selection. + if (!selection_.collapsed()) { + selection_ = TextRange(selection_.start()); return true; } - // If not at the start, move the beginning backward. - if (selection_base_ != text_.begin()) { - int count = IsTrailingSurrogate(*(selection_base_ - 1)) ? 2 : 1; - selection_base_ -= count; - selection_extent_ = selection_base_; + // Otherwise, move the cursor backward. + size_t position = selection_.position(); + if (position != editable_range().start()) { + int count = IsTrailingSurrogate(text_.at(position - 1)) ? 2 : 1; + selection_ = TextRange(position - count); return true; } return false; @@ -216,9 +276,9 @@ std::string TextInputModel::GetText() const { } int TextInputModel::GetCursorOffset() const { - // Measure the length of the current text up to the cursor. + // Measure the length of the current text up to the selection extent. // There is probably a much more efficient way of doing this. - auto leading_text = text_.substr(0, selection_extent_ - text_.begin()); + auto leading_text = text_.substr(0, selection_.extent()); std::wstring_convert, char16_t> utf8_converter; return utf8_converter.to_bytes(leading_text).size(); diff --git a/shell/platform/common/cpp/text_input_model.h b/shell/platform/common/cpp/text_input_model.h index 2de5379551061..a9560b7b1483d 100644 --- a/shell/platform/common/cpp/text_input_model.h +++ b/shell/platform/common/cpp/text_input_model.h @@ -8,7 +8,10 @@ #include #include +#include "flutter/shell/platform/common/cpp/text_range.h" + namespace flutter { + // Handles underlying text input state, using a simple ASCII model. // // Ignores special states like "insert mode" for now. @@ -24,8 +27,43 @@ class TextInputModel { // Attempts to set the text selection. // - // Returns false if the base or extent are out of bounds. - bool SetSelection(size_t base, size_t extent); + // Returns false if the selection is not within the bounds of the text. + // While in composing mode, the selection is restricted to the composing + // range; otherwise, it is restricted to the length of the text. + bool SetSelection(const TextRange& range); + + // Attempts to set the composing range. + // + // Returns false if the range or offset are out of range for the text, or if + // the offset is outside the composing range. + bool SetComposingRange(const TextRange& range, size_t cursor_offset); + + // Begins IME composing mode. + // + // Resets the composing base and extent to the selection start. The existing + // selection is preserved in case composing is aborted with no changes. Until + // |EndComposing| is called, any further changes to selection base and extent + // are restricted to the composing range. + void BeginComposing(); + + // Replaces the composing range with new text. + // + // If a selection of non-zero length exists, it is deleted if the composing + // text is non-empty. The composing range is adjusted to the length of + // |composing_text| and the selection base and offset are set to the end of + // the composing range. + void UpdateComposingText(const std::string& composing_text); + + // Commits composing range to the string. + // + // Causes the composing base and extent to be collapsed to the end of the + // range. + void CommitComposing(); + + // Ends IME composing mode. + // + // Collapses the composing base and offset to 0. + void EndComposing(); // Adds a Unicode code point. // @@ -49,18 +87,21 @@ class TextInputModel { // Deletes either the selection, or one character ahead of the cursor. // // Deleting one character ahead of the cursor occurs when the selection base - // and extent are the same. + // and extent are the same. When composing is active, deletions are + // restricted to text between the composing base and extent. // // Returns true if any deletion actually occurred. bool Delete(); // Deletes text near the cursor. // - // A section is made starting at @offset code points past the cursor (negative - // values go before the cursor). @count code points are removed. The selection - // may go outside the bounds of the text and will result in only the part - // selection that covers the available text being deleted. The existing - // selection is ignored and removed after this operation. + // A section is made starting at |offset_from_cursor| code points past the + // cursor (negative values go before the cursor). |count| code points are + // removed. The selection may go outside the bounds of the available text and + // will result in only the part selection that covers the available text + // being deleted. The existing selection is ignored and removed after this + // operation. When composing is active, deletions are restricted to the + // composing range. // // Returns true if any deletion actually occurred. bool DeleteSurrounding(int offset_from_cursor, int count); @@ -68,7 +109,8 @@ class TextInputModel { // Deletes either the selection, or one character behind the cursor. // // Deleting one character behind the cursor occurs when the selection base - // and extent are the same. + // and extent are the same. When composing is active, deletions are + // restricted to the text between the composing base and extent. // // Returns true if any deletion actually occurred. bool Backspace(); @@ -76,21 +118,31 @@ class TextInputModel { // Attempts to move the cursor backward. // // Returns true if the cursor could be moved. If a selection is active, moves - // to the start of the selection. + // to the start of the selection. If composing is active, motion is + // restricted to the composing range. bool MoveCursorBack(); // Attempts to move the cursor forward. // // Returns true if the cursor could be moved. If a selection is active, moves - // to the end of the selection. + // to the end of the selection. If composing is active, motion is restricted + // to the composing range. bool MoveCursorForward(); // Attempts to move the cursor to the beginning. // + // If composing is active, the cursor is moved to the beginning of the + // composing range; otherwise, it is moved to the beginning of the text. If + // composing is active, motion is restricted to the composing range. + // // Returns true if the cursor could be moved. bool MoveCursorToBeginning(); - // Attempts to move the cursor to the back. + // Attempts to move the cursor to the end. + // + // If composing is active, the cursor is moved to the end of the composing + // range; otherwise, it is moved to the end of the text. If composing is + // active, motion is restricted to the composing range. // // Returns true if the cursor could be moved. bool MoveCursorToEnd(); @@ -102,34 +154,39 @@ class TextInputModel { // GetText(). int GetCursorOffset() const; - // The position where the selection starts. - int selection_base() const { - return static_cast(selection_base_ - text_.begin()); - } + // The current selection. + TextRange selection() const { return selection_; } - // The position of the cursor. - int selection_extent() const { - return static_cast(selection_extent_ - text_.begin()); - } + // The composing range. + // + // If not in composing mode, returns a collapsed range at position 0. + TextRange composing_range() const { return composing_range_; } - private: - void DeleteSelected(); + // Whether multi-step input composing mode is active. + bool composing() const { return composing_; } - std::u16string text_; - std::u16string::iterator selection_base_; - std::u16string::iterator selection_extent_; + private: + // Deletes the current selection, if any. + // + // Returns true if any text is deleted. The selection base and extent are + // reset to the start of the selected range. + bool DeleteSelected(); - // Returns the left hand side of the selection. - std::u16string::iterator selection_start() { - return selection_base_ < selection_extent_ ? selection_base_ - : selection_extent_; + // Returns the currently editable text range. + // + // In composing mode, returns the composing range; otherwise, returns a range + // covering the entire text. + TextRange editable_range() const { + return composing_ ? composing_range_ : text_range(); } - // Returns the right hand side of the selection. - std::u16string::iterator selection_end() { - return selection_base_ > selection_extent_ ? selection_base_ - : selection_extent_; - } + // Returns a range covering the entire text. + TextRange text_range() const { return TextRange(0, text_.length()); } + + std::u16string text_; + TextRange selection_ = TextRange(0); + TextRange composing_range_ = TextRange(0); + bool composing_ = false; }; } // namespace flutter diff --git a/shell/platform/common/cpp/text_input_model_unittests.cc b/shell/platform/common/cpp/text_input_model_unittests.cc index 86c4771cd5246..6ee80206295eb 100644 --- a/shell/platform/common/cpp/text_input_model_unittests.cc +++ b/shell/platform/common/cpp/text_input_model_unittests.cc @@ -41,65 +41,366 @@ TEST(TextInputModel, SetTextReplaceText) { TEST(TextInputModel, SetTextResetsSelection) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(3, 3)); - EXPECT_EQ(model->selection_base(), 3); - EXPECT_EQ(model->selection_extent(), 3); + EXPECT_TRUE(model->SetSelection(TextRange(3))); + EXPECT_EQ(model->selection(), TextRange(3)); model->SetText("FGHJI"); - EXPECT_EQ(model->selection_base(), 0); - EXPECT_EQ(model->selection_extent(), 0); + EXPECT_EQ(model->selection(), TextRange(0)); } TEST(TextInputModel, SetSelectionStart) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(0, 0)); - EXPECT_EQ(model->selection_base(), 0); - EXPECT_EQ(model->selection_extent(), 0); + EXPECT_TRUE(model->SetSelection(TextRange(0))); + EXPECT_EQ(model->selection(), TextRange(0)); + EXPECT_EQ(model->composing_range(), TextRange(0)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, SetSelectionComposingStart) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 0)); + EXPECT_TRUE(model->SetSelection(TextRange(1))); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(1, 4)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } TEST(TextInputModel, SetSelectionMiddle) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(2, 2)); - EXPECT_EQ(model->selection_base(), 2); - EXPECT_EQ(model->selection_extent(), 2); + EXPECT_TRUE(model->SetSelection(TextRange(2))); + EXPECT_EQ(model->selection(), TextRange(2)); + EXPECT_EQ(model->composing_range(), TextRange(0)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, SetSelectionComposingMiddle) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 0)); + EXPECT_TRUE(model->SetSelection(TextRange(2))); + EXPECT_EQ(model->selection(), TextRange(2)); + EXPECT_EQ(model->composing_range(), TextRange(1, 4)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } TEST(TextInputModel, SetSelectionEnd) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(5, 5)); - EXPECT_EQ(model->selection_base(), 5); - EXPECT_EQ(model->selection_extent(), 5); + EXPECT_TRUE(model->SetSelection(TextRange(5))); + EXPECT_EQ(model->selection(), TextRange(5)); + EXPECT_EQ(model->composing_range(), TextRange(0)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, SetSelectionComposingEnd) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 0)); + EXPECT_TRUE(model->SetSelection(TextRange(4))); + EXPECT_EQ(model->selection(), TextRange(4)); + EXPECT_EQ(model->composing_range(), TextRange(1, 4)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } TEST(TextInputModel, SetSelectionWthExtent) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(1, 4)); - EXPECT_EQ(model->selection_base(), 1); - EXPECT_EQ(model->selection_extent(), 4); + EXPECT_TRUE(model->SetSelection(TextRange(1, 4))); + EXPECT_EQ(model->selection(), TextRange(1, 4)); + EXPECT_EQ(model->composing_range(), TextRange(0)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, SetSelectionWthExtentComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 0)); + EXPECT_FALSE(model->SetSelection(TextRange(1, 4))); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(1, 4)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } TEST(TextInputModel, SetSelectionReverseExtent) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(4, 1)); - EXPECT_EQ(model->selection_base(), 4); - EXPECT_EQ(model->selection_extent(), 1); + EXPECT_TRUE(model->SetSelection(TextRange(4, 1))); + EXPECT_EQ(model->selection(), TextRange(4, 1)); + EXPECT_EQ(model->composing_range(), TextRange(0)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, SetSelectionReverseExtentComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 0)); + EXPECT_FALSE(model->SetSelection(TextRange(4, 1))); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(1, 4)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } TEST(TextInputModel, SetSelectionOutsideString) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_FALSE(model->SetSelection(4, 6)); - EXPECT_FALSE(model->SetSelection(5, 6)); - EXPECT_FALSE(model->SetSelection(6, 6)); + EXPECT_FALSE(model->SetSelection(TextRange(4, 6))); + EXPECT_FALSE(model->SetSelection(TextRange(5, 6))); + EXPECT_FALSE(model->SetSelection(TextRange(6))); +} + +TEST(TextInputModel, SetSelectionOutsideComposingRange) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 0)); + EXPECT_FALSE(model->SetSelection(TextRange(0))); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_FALSE(model->SetSelection(TextRange(5))); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(1, 4)); +} + +TEST(TextInputModel, SetComposingRangeStart) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(0, 0), 0)); + EXPECT_EQ(model->selection(), TextRange(0)); + EXPECT_EQ(model->composing_range(), TextRange(0)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, SetComposingRangeMiddle) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(2, 2), 0)); + EXPECT_EQ(model->selection(), TextRange(2)); + EXPECT_EQ(model->composing_range(), TextRange(2)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, SetComposingRangeEnd) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(5, 5), 0)); + EXPECT_EQ(model->selection(), TextRange(5)); + EXPECT_EQ(model->composing_range(), TextRange(5)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, SetComposingRangeWithExtent) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 3)); + EXPECT_EQ(model->selection(), TextRange(4)); + EXPECT_EQ(model->composing_range(), TextRange(1, 4)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, SetComposingRangeReverseExtent) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(4, 1), 3)); + EXPECT_EQ(model->selection(), TextRange(4)); + EXPECT_EQ(model->composing_range(), TextRange(4, 1)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, SetComposingRangeOutsideString) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_FALSE(model->SetComposingRange(TextRange(4, 6), 0)); + EXPECT_FALSE(model->SetComposingRange(TextRange(5, 6), 0)); + EXPECT_FALSE(model->SetComposingRange(TextRange(6, 6), 0)); +} + +// Composing sequence with no initial selection and no text input. +TEST(TextInputModel, CommitComposingNoTextWithNoSelection) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->SetSelection(TextRange(0)); + + // Verify no changes on BeginComposing. + model->BeginComposing(); + EXPECT_EQ(model->selection(), TextRange(0)); + EXPECT_EQ(model->composing_range(), TextRange(0)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); + + // Verify no changes on CommitComposing. + model->CommitComposing(); + EXPECT_EQ(model->selection(), TextRange(0)); + EXPECT_EQ(model->composing_range(), TextRange(0)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); + + // Verify no changes on CommitComposing. + model->EndComposing(); + EXPECT_EQ(model->selection(), TextRange(0)); + EXPECT_EQ(model->composing_range(), TextRange(0)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +// Composing sequence with an initial selection and no text input. +TEST(TextInputModel, CommitComposingNoTextWithSelection) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->SetSelection(TextRange(1, 3)); + + // Verify no changes on BeginComposing. + model->BeginComposing(); + EXPECT_EQ(model->selection(), TextRange(1, 3)); + EXPECT_EQ(model->composing_range(), TextRange(1)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); + + // Verify no changes on CommitComposing. + model->CommitComposing(); + EXPECT_EQ(model->selection(), TextRange(1, 3)); + EXPECT_EQ(model->composing_range(), TextRange(1)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); + + // Verify no changes on CommitComposing. + model->EndComposing(); + EXPECT_EQ(model->selection(), TextRange(1, 3)); + EXPECT_EQ(model->composing_range(), TextRange(0)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +// Composing sequence with no initial selection. +TEST(TextInputModel, CommitComposingTextWithNoSelection) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->SetSelection(TextRange(1)); + + // Verify no changes on BeginComposing. + model->BeginComposing(); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(1)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); + + // Verify selection base, extent and composing extent increment as text is + // entered. Verify composing base does not change. + model->UpdateComposingText("つ"); + EXPECT_EQ(model->selection(), TextRange(2)); + EXPECT_EQ(model->composing_range(), TextRange(1, 2)); + EXPECT_STREQ(model->GetText().c_str(), "AつBCDE"); + model->UpdateComposingText("つる"); + EXPECT_EQ(model->selection(), TextRange(3)); + EXPECT_EQ(model->composing_range(), TextRange(1, 3)); + EXPECT_STREQ(model->GetText().c_str(), "AつるBCDE"); + + // Verify that cursor position is set to correct offset from composing base. + model->UpdateComposingText("鶴"); + EXPECT_TRUE(model->SetSelection(TextRange(1))); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(1, 2)); + EXPECT_STREQ(model->GetText().c_str(), "A鶴BCDE"); + + // Verify composing base is set to composing extent on commit. + model->CommitComposing(); + EXPECT_EQ(model->selection(), TextRange(2)); + EXPECT_EQ(model->composing_range(), TextRange(2)); + EXPECT_STREQ(model->GetText().c_str(), "A鶴BCDE"); + + // Verify that further text entry increments the selection base, extent and + // the composing extent. Verify that composing base does not change. + model->UpdateComposingText("が"); + EXPECT_EQ(model->selection(), TextRange(3)); + EXPECT_EQ(model->composing_range(), TextRange(2, 3)); + EXPECT_STREQ(model->GetText().c_str(), "A鶴がBCDE"); + + // Verify composing base is set to composing extent on commit. + model->CommitComposing(); + EXPECT_EQ(model->selection(), TextRange(3)); + EXPECT_EQ(model->composing_range(), TextRange(3)); + EXPECT_STREQ(model->GetText().c_str(), "A鶴がBCDE"); + + // Verify no changes on EndComposing. + model->EndComposing(); + EXPECT_EQ(model->selection(), TextRange(3)); + EXPECT_EQ(model->composing_range(), TextRange(0)); + EXPECT_STREQ(model->GetText().c_str(), "A鶴がBCDE"); +} + +// Composing sequence with an initial selection. +TEST(TextInputModel, CommitComposingTextWithSelection) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->SetSelection(TextRange(1, 3)); + + // Verify no changes on BeginComposing. + model->BeginComposing(); + EXPECT_EQ(model->selection(), TextRange(1, 3)); + EXPECT_EQ(model->composing_range(), TextRange(1)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); + + // Verify selection is replaced and selection base, extent and composing + // extent increment to the position immediately after the composing text. + // Verify composing base does not change. + model->UpdateComposingText("つ"); + EXPECT_EQ(model->selection(), TextRange(2)); + EXPECT_EQ(model->composing_range(), TextRange(1, 2)); + EXPECT_STREQ(model->GetText().c_str(), "AつDE"); + + // Verify that further text entry increments the selection base, extent and + // the composing extent. Verify that composing base does not change. + model->UpdateComposingText("つる"); + EXPECT_EQ(model->selection(), TextRange(3)); + EXPECT_EQ(model->composing_range(), TextRange(1, 3)); + EXPECT_STREQ(model->GetText().c_str(), "AつるDE"); + + // Verify that cursor position is set to correct offset from composing base. + model->UpdateComposingText("鶴"); + EXPECT_TRUE(model->SetSelection(TextRange(1))); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(1, 2)); + EXPECT_STREQ(model->GetText().c_str(), "A鶴DE"); + + // Verify composing base is set to composing extent on commit. + model->CommitComposing(); + EXPECT_EQ(model->selection(), TextRange(2)); + EXPECT_EQ(model->composing_range(), TextRange(2)); + EXPECT_STREQ(model->GetText().c_str(), "A鶴DE"); + + // Verify that further text entry increments the selection base, extent and + // the composing extent. Verify that composing base does not change. + model->UpdateComposingText("が"); + EXPECT_EQ(model->selection(), TextRange(3)); + EXPECT_EQ(model->composing_range(), TextRange(2, 3)); + EXPECT_STREQ(model->GetText().c_str(), "A鶴がDE"); + + // Verify composing base is set to composing extent on commit. + model->CommitComposing(); + EXPECT_EQ(model->selection(), TextRange(3)); + EXPECT_EQ(model->composing_range(), TextRange(3)); + EXPECT_STREQ(model->GetText().c_str(), "A鶴がDE"); + + // Verify no changes on EndComposing. + model->EndComposing(); + EXPECT_EQ(model->selection(), TextRange(3)); + EXPECT_EQ(model->composing_range(), TextRange(0)); + EXPECT_STREQ(model->GetText().c_str(), "A鶴がDE"); +} + +TEST(TextInputModel, UpdateComposingRemovesLastComposingCharacter) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + model->SetComposingRange(TextRange(1, 2), 1); + model->UpdateComposingText(""); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(1)); + model->SetText("ACDE"); } TEST(TextInputModel, AddCodePoint) { @@ -109,48 +410,48 @@ TEST(TextInputModel, AddCodePoint) { model->AddCodePoint(0x1f604); model->AddCodePoint('D'); model->AddCodePoint('E'); - EXPECT_EQ(model->selection_base(), 6); - EXPECT_EQ(model->selection_extent(), 6); + EXPECT_EQ(model->selection(), TextRange(6)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "AB😄DE"); } TEST(TextInputModel, AddCodePointSelection) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(1, 4)); + EXPECT_TRUE(model->SetSelection(TextRange(1, 4))); model->AddCodePoint('x'); - EXPECT_EQ(model->selection_base(), 2); - EXPECT_EQ(model->selection_extent(), 2); + EXPECT_EQ(model->selection(), TextRange(2)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "AxE"); } TEST(TextInputModel, AddCodePointReverseSelection) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(4, 1)); + EXPECT_TRUE(model->SetSelection(TextRange(4, 1))); model->AddCodePoint('x'); - EXPECT_EQ(model->selection_base(), 2); - EXPECT_EQ(model->selection_extent(), 2); + EXPECT_EQ(model->selection(), TextRange(2)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "AxE"); } TEST(TextInputModel, AddCodePointSelectionWideCharacter) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(1, 4)); + EXPECT_TRUE(model->SetSelection(TextRange(1, 4))); model->AddCodePoint(0x1f604); - EXPECT_EQ(model->selection_base(), 3); - EXPECT_EQ(model->selection_extent(), 3); + EXPECT_EQ(model->selection(), TextRange(3)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "A😄E"); } TEST(TextInputModel, AddCodePointReverseSelectionWideCharacter) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(4, 1)); + EXPECT_TRUE(model->SetSelection(TextRange(4, 1))); model->AddCodePoint(0x1f604); - EXPECT_EQ(model->selection_base(), 3); - EXPECT_EQ(model->selection_extent(), 3); + EXPECT_EQ(model->selection(), TextRange(3)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "A😄E"); } @@ -159,498 +460,999 @@ TEST(TextInputModel, AddText) { model->AddText(u"ABCDE"); model->AddText("😄"); model->AddText("FGHIJ"); - EXPECT_EQ(model->selection_base(), 12); - EXPECT_EQ(model->selection_extent(), 12); + EXPECT_EQ(model->selection(), TextRange(12)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE😄FGHIJ"); } TEST(TextInputModel, AddTextSelection) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(1, 4)); + EXPECT_TRUE(model->SetSelection(TextRange(1, 4))); model->AddText("xy"); - EXPECT_EQ(model->selection_base(), 3); - EXPECT_EQ(model->selection_extent(), 3); + EXPECT_EQ(model->selection(), TextRange(3)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "AxyE"); } TEST(TextInputModel, AddTextReverseSelection) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(4, 1)); + EXPECT_TRUE(model->SetSelection(TextRange(4, 1))); model->AddText("xy"); - EXPECT_EQ(model->selection_base(), 3); - EXPECT_EQ(model->selection_extent(), 3); + EXPECT_EQ(model->selection(), TextRange(3)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "AxyE"); } TEST(TextInputModel, AddTextSelectionWideCharacter) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(1, 4)); + EXPECT_TRUE(model->SetSelection(TextRange(1, 4))); model->AddText(u"😄🙃"); - EXPECT_EQ(model->selection_base(), 5); - EXPECT_EQ(model->selection_extent(), 5); + EXPECT_EQ(model->selection(), TextRange(5)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "A😄🙃E"); } TEST(TextInputModel, AddTextReverseSelectionWideCharacter) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(4, 1)); + EXPECT_TRUE(model->SetSelection(TextRange(4, 1))); model->AddText(u"😄🙃"); - EXPECT_EQ(model->selection_base(), 5); - EXPECT_EQ(model->selection_extent(), 5); + EXPECT_EQ(model->selection(), TextRange(5)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "A😄🙃E"); } TEST(TextInputModel, DeleteStart) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(0, 0)); + EXPECT_TRUE(model->SetSelection(TextRange(0))); ASSERT_TRUE(model->Delete()); - EXPECT_EQ(model->selection_base(), 0); - EXPECT_EQ(model->selection_extent(), 0); + EXPECT_EQ(model->selection(), TextRange(0)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "BCDE"); } TEST(TextInputModel, DeleteMiddle) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(2, 2)); + EXPECT_TRUE(model->SetSelection(TextRange(2))); ASSERT_TRUE(model->Delete()); - EXPECT_EQ(model->selection_base(), 2); - EXPECT_EQ(model->selection_extent(), 2); + EXPECT_EQ(model->selection(), TextRange(2)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ABDE"); } TEST(TextInputModel, DeleteEnd) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(5, 5)); + EXPECT_TRUE(model->SetSelection(TextRange(5))); ASSERT_FALSE(model->Delete()); - EXPECT_EQ(model->selection_base(), 5); - EXPECT_EQ(model->selection_extent(), 5); + EXPECT_EQ(model->selection(), TextRange(5)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } TEST(TextInputModel, DeleteWideCharacters) { auto model = std::make_unique(); model->SetText("😄🙃🤪🧐"); - EXPECT_TRUE(model->SetSelection(4, 4)); + EXPECT_TRUE(model->SetSelection(TextRange(4))); ASSERT_TRUE(model->Delete()); - EXPECT_EQ(model->selection_base(), 4); - EXPECT_EQ(model->selection_extent(), 4); + EXPECT_EQ(model->selection(), TextRange(4)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "😄🙃🧐"); } TEST(TextInputModel, DeleteSelection) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(1, 4)); + EXPECT_TRUE(model->SetSelection(TextRange(1, 4))); ASSERT_TRUE(model->Delete()); - EXPECT_EQ(model->selection_base(), 1); - EXPECT_EQ(model->selection_extent(), 1); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "AE"); } TEST(TextInputModel, DeleteReverseSelection) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(4, 1)); + EXPECT_TRUE(model->SetSelection(TextRange(4, 1))); ASSERT_TRUE(model->Delete()); - EXPECT_EQ(model->selection_base(), 1); - EXPECT_EQ(model->selection_extent(), 1); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "AE"); } +TEST(TextInputModel, DeleteStartComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 0)); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(1, 4)); + ASSERT_TRUE(model->Delete()); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(1, 3)); + EXPECT_STREQ(model->GetText().c_str(), "ACDE"); +} + +TEST(TextInputModel, DeleteStartReverseComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(4, 1), 0)); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(4, 1)); + ASSERT_TRUE(model->Delete()); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(3, 1)); + EXPECT_STREQ(model->GetText().c_str(), "ACDE"); +} + +TEST(TextInputModel, DeleteMiddleComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 1)); + ASSERT_TRUE(model->Delete()); + EXPECT_EQ(model->selection(), TextRange(2)); + EXPECT_EQ(model->composing_range(), TextRange(1, 3)); + EXPECT_STREQ(model->GetText().c_str(), "ABDE"); +} + +TEST(TextInputModel, DeleteMiddleReverseComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(4, 1), 1)); + ASSERT_TRUE(model->Delete()); + EXPECT_EQ(model->selection(), TextRange(2)); + EXPECT_EQ(model->composing_range(), TextRange(3, 1)); + EXPECT_STREQ(model->GetText().c_str(), "ABDE"); +} + +TEST(TextInputModel, DeleteEndComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 3)); + ASSERT_FALSE(model->Delete()); + EXPECT_EQ(model->selection(), TextRange(4)); + EXPECT_EQ(model->composing_range(), TextRange(1, 4)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, DeleteEndReverseComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(4, 1), 3)); + ASSERT_FALSE(model->Delete()); + EXPECT_EQ(model->selection(), TextRange(4)); + EXPECT_EQ(model->composing_range(), TextRange(4, 1)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + TEST(TextInputModel, DeleteSurroundingAtCursor) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(2, 2)); + EXPECT_TRUE(model->SetSelection(TextRange(2))); + EXPECT_TRUE(model->DeleteSurrounding(0, 1)); + EXPECT_EQ(model->selection(), TextRange(2)); + EXPECT_EQ(model->composing_range(), TextRange(0)); + EXPECT_STREQ(model->GetText().c_str(), "ABDE"); +} + +TEST(TextInputModel, DeleteSurroundingAtCursorComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 1)); EXPECT_TRUE(model->DeleteSurrounding(0, 1)); - EXPECT_EQ(model->selection_base(), 2); - EXPECT_EQ(model->selection_extent(), 2); + EXPECT_EQ(model->selection(), TextRange(2)); + EXPECT_EQ(model->composing_range(), TextRange(1, 3)); EXPECT_STREQ(model->GetText().c_str(), "ABDE"); } TEST(TextInputModel, DeleteSurroundingAtCursorAll) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(2, 2)); + EXPECT_TRUE(model->SetSelection(TextRange(2))); EXPECT_TRUE(model->DeleteSurrounding(0, 3)); - EXPECT_EQ(model->selection_base(), 2); - EXPECT_EQ(model->selection_extent(), 2); + EXPECT_EQ(model->selection(), TextRange(2)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "AB"); } +TEST(TextInputModel, DeleteSurroundingAtCursorAllComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 1)); + EXPECT_TRUE(model->DeleteSurrounding(0, 2)); + EXPECT_EQ(model->selection(), TextRange(2)); + EXPECT_EQ(model->composing_range(), TextRange(1, 2)); + EXPECT_STREQ(model->GetText().c_str(), "ABE"); +} + TEST(TextInputModel, DeleteSurroundingAtCursorGreedy) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(2, 2)); + EXPECT_TRUE(model->SetSelection(TextRange(2))); EXPECT_TRUE(model->DeleteSurrounding(0, 4)); - EXPECT_EQ(model->selection_base(), 2); - EXPECT_EQ(model->selection_extent(), 2); + EXPECT_EQ(model->selection(), TextRange(2)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "AB"); } +TEST(TextInputModel, DeleteSurroundingAtCursorGreedyComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 1)); + EXPECT_TRUE(model->DeleteSurrounding(0, 4)); + EXPECT_EQ(model->selection(), TextRange(2)); + EXPECT_EQ(model->composing_range(), TextRange(1, 2)); + EXPECT_STREQ(model->GetText().c_str(), "ABE"); +} + TEST(TextInputModel, DeleteSurroundingBeforeCursor) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(2, 2)); + EXPECT_TRUE(model->SetSelection(TextRange(2))); EXPECT_TRUE(model->DeleteSurrounding(-1, 1)); - EXPECT_EQ(model->selection_base(), 1); - EXPECT_EQ(model->selection_extent(), 1); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ACDE"); } +TEST(TextInputModel, DeleteSurroundingBeforeCursorComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 2)); + EXPECT_TRUE(model->DeleteSurrounding(-1, 1)); + EXPECT_EQ(model->selection(), TextRange(2)); + EXPECT_EQ(model->composing_range(), TextRange(1, 3)); + EXPECT_STREQ(model->GetText().c_str(), "ABDE"); +} + TEST(TextInputModel, DeleteSurroundingBeforeCursorAll) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(2, 2)); + EXPECT_TRUE(model->SetSelection(TextRange(2))); EXPECT_TRUE(model->DeleteSurrounding(-2, 2)); - EXPECT_EQ(model->selection_base(), 0); - EXPECT_EQ(model->selection_extent(), 0); + EXPECT_EQ(model->selection(), TextRange(0)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "CDE"); } +TEST(TextInputModel, DeleteSurroundingBeforeCursorAllComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 2)); + EXPECT_TRUE(model->DeleteSurrounding(-2, 2)); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(1, 2)); + EXPECT_STREQ(model->GetText().c_str(), "ADE"); +} + TEST(TextInputModel, DeleteSurroundingBeforeCursorGreedy) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(2, 2)); + EXPECT_TRUE(model->SetSelection(TextRange(2))); EXPECT_TRUE(model->DeleteSurrounding(-3, 3)); - EXPECT_EQ(model->selection_base(), 0); - EXPECT_EQ(model->selection_extent(), 0); + EXPECT_EQ(model->selection(), TextRange(0)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "CDE"); } +TEST(TextInputModel, DeleteSurroundingBeforeCursorGreedyComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 2)); + EXPECT_TRUE(model->DeleteSurrounding(-3, 3)); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(1, 2)); + EXPECT_STREQ(model->GetText().c_str(), "ADE"); +} + TEST(TextInputModel, DeleteSurroundingAfterCursor) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(2, 2)); + EXPECT_TRUE(model->SetSelection(TextRange(2))); EXPECT_TRUE(model->DeleteSurrounding(1, 1)); - EXPECT_EQ(model->selection_base(), 2); - EXPECT_EQ(model->selection_extent(), 2); + EXPECT_EQ(model->selection(), TextRange(2)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ABCE"); } +TEST(TextInputModel, DeleteSurroundingAfterCursorComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 0)); + EXPECT_TRUE(model->DeleteSurrounding(1, 1)); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(1, 3)); + EXPECT_STREQ(model->GetText().c_str(), "ABDE"); +} + TEST(TextInputModel, DeleteSurroundingAfterCursorAll) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(2, 2)); + EXPECT_TRUE(model->SetSelection(TextRange(2))); EXPECT_TRUE(model->DeleteSurrounding(1, 2)); - EXPECT_EQ(model->selection_base(), 2); - EXPECT_EQ(model->selection_extent(), 2); + EXPECT_EQ(model->selection(), TextRange(2)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ABC"); } +TEST(TextInputModel, DeleteSurroundingAfterCursorAllComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 0)); + EXPECT_TRUE(model->DeleteSurrounding(1, 2)); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(1, 2)); + EXPECT_STREQ(model->GetText().c_str(), "ABE"); +} + TEST(TextInputModel, DeleteSurroundingAfterCursorGreedy) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(2, 2)); + EXPECT_TRUE(model->SetSelection(TextRange(2))); EXPECT_TRUE(model->DeleteSurrounding(1, 3)); - EXPECT_EQ(model->selection_base(), 2); - EXPECT_EQ(model->selection_extent(), 2); + EXPECT_EQ(model->selection(), TextRange(2)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ABC"); } +TEST(TextInputModel, DeleteSurroundingAfterCursorGreedyComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 0)); + EXPECT_TRUE(model->DeleteSurrounding(1, 3)); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(1, 2)); + EXPECT_STREQ(model->GetText().c_str(), "ABE"); +} + TEST(TextInputModel, DeleteSurroundingSelection) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(2, 3)); + EXPECT_TRUE(model->SetSelection(TextRange(2, 3))); EXPECT_TRUE(model->DeleteSurrounding(0, 1)); - EXPECT_EQ(model->selection_base(), 3); - EXPECT_EQ(model->selection_extent(), 3); + EXPECT_EQ(model->selection(), TextRange(3)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ABCE"); } TEST(TextInputModel, DeleteSurroundingReverseSelection) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(4, 3)); + EXPECT_TRUE(model->SetSelection(TextRange(4, 3))); EXPECT_TRUE(model->DeleteSurrounding(0, 1)); - EXPECT_EQ(model->selection_base(), 3); - EXPECT_EQ(model->selection_extent(), 3); + EXPECT_EQ(model->selection(), TextRange(3)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ABCE"); } TEST(TextInputModel, BackspaceStart) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(0, 0)); + EXPECT_TRUE(model->SetSelection(TextRange(0))); ASSERT_FALSE(model->Backspace()); - EXPECT_EQ(model->selection_base(), 0); - EXPECT_EQ(model->selection_extent(), 0); + EXPECT_EQ(model->selection(), TextRange(0)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } TEST(TextInputModel, BackspaceMiddle) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(2, 2)); + EXPECT_TRUE(model->SetSelection(TextRange(2))); ASSERT_TRUE(model->Backspace()); - EXPECT_EQ(model->selection_base(), 1); - EXPECT_EQ(model->selection_extent(), 1); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ACDE"); } TEST(TextInputModel, BackspaceEnd) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(5, 5)); + EXPECT_TRUE(model->SetSelection(TextRange(5))); ASSERT_TRUE(model->Backspace()); - EXPECT_EQ(model->selection_base(), 4); - EXPECT_EQ(model->selection_extent(), 4); + EXPECT_EQ(model->selection(), TextRange(4)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ABCD"); } TEST(TextInputModel, BackspaceWideCharacters) { auto model = std::make_unique(); model->SetText("😄🙃🤪🧐"); - EXPECT_TRUE(model->SetSelection(4, 4)); + EXPECT_TRUE(model->SetSelection(TextRange(4))); ASSERT_TRUE(model->Backspace()); - EXPECT_EQ(model->selection_base(), 2); - EXPECT_EQ(model->selection_extent(), 2); + EXPECT_EQ(model->selection(), TextRange(2)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "😄🤪🧐"); } TEST(TextInputModel, BackspaceSelection) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(1, 4)); + EXPECT_TRUE(model->SetSelection(TextRange(1, 4))); ASSERT_TRUE(model->Delete()); - EXPECT_EQ(model->selection_base(), 1); - EXPECT_EQ(model->selection_extent(), 1); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "AE"); } TEST(TextInputModel, BackspaceReverseSelection) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(4, 1)); + EXPECT_TRUE(model->SetSelection(TextRange(4, 1))); ASSERT_TRUE(model->Delete()); - EXPECT_EQ(model->selection_base(), 1); - EXPECT_EQ(model->selection_extent(), 1); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "AE"); } +TEST(TextInputModel, BackspaceStartComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 0)); + ASSERT_FALSE(model->Backspace()); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(1, 4)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, BackspaceStartReverseComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(4, 1), 0)); + ASSERT_FALSE(model->Backspace()); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(4, 1)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, BackspaceMiddleComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 1)); + ASSERT_TRUE(model->Backspace()); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(1, 3)); + EXPECT_STREQ(model->GetText().c_str(), "ACDE"); +} + +TEST(TextInputModel, BackspaceMiddleReverseComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(4, 1), 1)); + ASSERT_TRUE(model->Backspace()); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(3, 1)); + EXPECT_STREQ(model->GetText().c_str(), "ACDE"); +} + +TEST(TextInputModel, BackspaceEndComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 3)); + ASSERT_TRUE(model->Backspace()); + EXPECT_EQ(model->selection(), TextRange(3)); + EXPECT_EQ(model->composing_range(), TextRange(1, 3)); + EXPECT_STREQ(model->GetText().c_str(), "ABCE"); +} + +TEST(TextInputModel, BackspaceEndReverseComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(4, 1), 3)); + ASSERT_TRUE(model->Backspace()); + EXPECT_EQ(model->selection(), TextRange(3)); + EXPECT_EQ(model->composing_range(), TextRange(3, 1)); + EXPECT_STREQ(model->GetText().c_str(), "ABCE"); +} + TEST(TextInputModel, MoveCursorForwardStart) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(0, 0)); + EXPECT_TRUE(model->SetSelection(TextRange(0))); EXPECT_TRUE(model->MoveCursorForward()); - EXPECT_EQ(model->selection_base(), 1); - EXPECT_EQ(model->selection_extent(), 1); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } TEST(TextInputModel, MoveCursorForwardMiddle) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(2, 2)); + EXPECT_TRUE(model->SetSelection(TextRange(2))); EXPECT_TRUE(model->MoveCursorForward()); - EXPECT_EQ(model->selection_base(), 3); - EXPECT_EQ(model->selection_extent(), 3); + EXPECT_EQ(model->selection(), TextRange(3)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } TEST(TextInputModel, MoveCursorForwardEnd) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(5, 5)); + EXPECT_TRUE(model->SetSelection(TextRange(5))); EXPECT_FALSE(model->MoveCursorForward()); - EXPECT_EQ(model->selection_base(), 5); - EXPECT_EQ(model->selection_extent(), 5); + EXPECT_EQ(model->selection(), TextRange(5)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } TEST(TextInputModel, MoveCursorForwardWideCharacters) { auto model = std::make_unique(); model->SetText("😄🙃🤪🧐"); - EXPECT_TRUE(model->SetSelection(4, 4)); + EXPECT_TRUE(model->SetSelection(TextRange(4))); ASSERT_TRUE(model->MoveCursorForward()); - EXPECT_EQ(model->selection_base(), 6); - EXPECT_EQ(model->selection_extent(), 6); + EXPECT_EQ(model->selection(), TextRange(6)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "😄🙃🤪🧐"); } TEST(TextInputModel, MoveCursorForwardSelection) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(1, 4)); + EXPECT_TRUE(model->SetSelection(TextRange(1, 4))); EXPECT_TRUE(model->MoveCursorForward()); - EXPECT_EQ(model->selection_base(), 4); - EXPECT_EQ(model->selection_extent(), 4); + EXPECT_EQ(model->selection(), TextRange(4)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } TEST(TextInputModel, MoveCursorForwardReverseSelection) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(4, 1)); + EXPECT_TRUE(model->SetSelection(TextRange(4, 1))); + EXPECT_TRUE(model->MoveCursorForward()); + EXPECT_EQ(model->selection(), TextRange(4)); + EXPECT_EQ(model->composing_range(), TextRange(0)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, MoveCursorForwardStartComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 0)); + EXPECT_TRUE(model->MoveCursorForward()); + EXPECT_EQ(model->selection(), TextRange(2)); + EXPECT_EQ(model->composing_range(), TextRange(1, 4)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, MoveCursorForwardStartReverseComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(4, 1), 0)); EXPECT_TRUE(model->MoveCursorForward()); - EXPECT_EQ(model->selection_base(), 4); - EXPECT_EQ(model->selection_extent(), 4); + EXPECT_EQ(model->selection(), TextRange(2)); + EXPECT_EQ(model->composing_range(), TextRange(4, 1)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, MoveCursorForwardMiddleComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 1)); + EXPECT_TRUE(model->MoveCursorForward()); + EXPECT_EQ(model->selection(), TextRange(3)); + EXPECT_EQ(model->composing_range(), TextRange(1, 4)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, MoveCursorForwardMiddleReverseComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(4, 1), 1)); + EXPECT_TRUE(model->MoveCursorForward()); + EXPECT_EQ(model->selection(), TextRange(3)); + EXPECT_EQ(model->composing_range(), TextRange(4, 1)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, MoveCursorForwardEndComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 3)); + EXPECT_FALSE(model->MoveCursorForward()); + EXPECT_EQ(model->selection(), TextRange(4)); + EXPECT_EQ(model->composing_range(), TextRange(1, 4)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, MoveCursorForwardEndReverseComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(4, 1), 3)); + EXPECT_FALSE(model->MoveCursorForward()); + EXPECT_EQ(model->selection(), TextRange(4)); + EXPECT_EQ(model->composing_range(), TextRange(4, 1)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } TEST(TextInputModel, MoveCursorBackStart) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(0, 0)); + EXPECT_TRUE(model->SetSelection(TextRange(0))); EXPECT_FALSE(model->MoveCursorBack()); - EXPECT_EQ(model->selection_base(), 0); - EXPECT_EQ(model->selection_extent(), 0); + EXPECT_EQ(model->selection(), TextRange(0)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } TEST(TextInputModel, MoveCursorBackMiddle) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(2, 2)); + EXPECT_TRUE(model->SetSelection(TextRange(2))); EXPECT_TRUE(model->MoveCursorBack()); - EXPECT_EQ(model->selection_base(), 1); - EXPECT_EQ(model->selection_extent(), 1); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } TEST(TextInputModel, MoveCursorBackEnd) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(5, 5)); + EXPECT_TRUE(model->SetSelection(TextRange(5))); EXPECT_TRUE(model->MoveCursorBack()); - EXPECT_EQ(model->selection_base(), 4); - EXPECT_EQ(model->selection_extent(), 4); + EXPECT_EQ(model->selection(), TextRange(4)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } TEST(TextInputModel, MoveCursorBackWideCharacters) { auto model = std::make_unique(); model->SetText("😄🙃🤪🧐"); - EXPECT_TRUE(model->SetSelection(4, 4)); + EXPECT_TRUE(model->SetSelection(TextRange(4))); ASSERT_TRUE(model->MoveCursorBack()); - EXPECT_EQ(model->selection_base(), 2); - EXPECT_EQ(model->selection_extent(), 2); + EXPECT_EQ(model->selection(), TextRange(2)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "😄🙃🤪🧐"); } TEST(TextInputModel, MoveCursorBackSelection) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(1, 4)); + EXPECT_TRUE(model->SetSelection(TextRange(1, 4))); EXPECT_TRUE(model->MoveCursorBack()); - EXPECT_EQ(model->selection_base(), 1); - EXPECT_EQ(model->selection_extent(), 1); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } TEST(TextInputModel, MoveCursorBackReverseSelection) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(4, 1)); + EXPECT_TRUE(model->SetSelection(TextRange(4, 1))); + EXPECT_TRUE(model->MoveCursorBack()); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(0)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, MoveCursorBackStartComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 0)); + EXPECT_TRUE(model->SetSelection(TextRange(1))); + EXPECT_FALSE(model->MoveCursorBack()); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(1, 4)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, MoveCursorBackStartReverseComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(4, 1), 0)); + EXPECT_TRUE(model->SetSelection(TextRange(1))); + EXPECT_FALSE(model->MoveCursorBack()); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(4, 1)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, MoveCursorBackMiddleComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 1)); EXPECT_TRUE(model->MoveCursorBack()); - EXPECT_EQ(model->selection_base(), 1); - EXPECT_EQ(model->selection_extent(), 1); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(1, 4)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, MoveCursorBackMiddleReverseComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(4, 1), 1)); + EXPECT_TRUE(model->MoveCursorBack()); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(4, 1)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, MoveCursorBackEndComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 3)); + EXPECT_TRUE(model->MoveCursorBack()); + EXPECT_EQ(model->selection(), TextRange(3)); + EXPECT_EQ(model->composing_range(), TextRange(1, 4)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, MoveCursorBackEndReverseComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(4, 1), 3)); + EXPECT_TRUE(model->MoveCursorBack()); + EXPECT_EQ(model->selection(), TextRange(3)); + EXPECT_EQ(model->composing_range(), TextRange(4, 1)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } TEST(TextInputModel, MoveCursorToBeginningStart) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(0, 0)); + EXPECT_TRUE(model->SetSelection(TextRange(0))); EXPECT_FALSE(model->MoveCursorToBeginning()); - EXPECT_EQ(model->selection_base(), 0); - EXPECT_EQ(model->selection_extent(), 0); + EXPECT_EQ(model->selection(), TextRange(0)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } TEST(TextInputModel, MoveCursorToBeginningMiddle) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(2, 2)); + EXPECT_TRUE(model->SetSelection(TextRange(2))); EXPECT_TRUE(model->MoveCursorToBeginning()); - EXPECT_EQ(model->selection_base(), 0); - EXPECT_EQ(model->selection_extent(), 0); + EXPECT_EQ(model->selection(), TextRange(0)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } TEST(TextInputModel, MoveCursorToBeginningEnd) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(5, 5)); + EXPECT_TRUE(model->SetSelection(TextRange(5))); EXPECT_TRUE(model->MoveCursorToBeginning()); - EXPECT_EQ(model->selection_base(), 0); - EXPECT_EQ(model->selection_extent(), 0); + EXPECT_EQ(model->selection(), TextRange(0)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } TEST(TextInputModel, MoveCursorToBeginningSelection) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(1, 4)); + EXPECT_TRUE(model->SetSelection(TextRange(1, 4))); EXPECT_TRUE(model->MoveCursorToBeginning()); - EXPECT_EQ(model->selection_base(), 0); - EXPECT_EQ(model->selection_extent(), 0); + EXPECT_EQ(model->selection(), TextRange(0)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } TEST(TextInputModel, MoveCursorToBeginningReverseSelection) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(4, 1)); + EXPECT_TRUE(model->SetSelection(TextRange(4, 1))); + EXPECT_TRUE(model->MoveCursorToBeginning()); + EXPECT_EQ(model->selection(), TextRange(0)); + EXPECT_EQ(model->composing_range(), TextRange(0)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, MoveCursorToBeginningStartComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 0)); + EXPECT_FALSE(model->MoveCursorToBeginning()); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(1, 4)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, MoveCursorToBeginningStartReverseComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(4, 1), 0)); + EXPECT_FALSE(model->MoveCursorToBeginning()); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(4, 1)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, MoveCursorToBeginningMiddleComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 1)); + EXPECT_TRUE(model->MoveCursorToBeginning()); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(1, 4)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, MoveCursorToBeginningMiddleReverseComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(4, 1), 1)); + EXPECT_TRUE(model->MoveCursorToBeginning()); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(4, 1)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, MoveCursorToBeginningEndComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 3)); EXPECT_TRUE(model->MoveCursorToBeginning()); - EXPECT_EQ(model->selection_base(), 0); - EXPECT_EQ(model->selection_extent(), 0); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(1, 4)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, MoveCursorToBeginningEndReverseComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(4, 1), 3)); + EXPECT_TRUE(model->MoveCursorToBeginning()); + EXPECT_EQ(model->selection(), TextRange(1)); + EXPECT_EQ(model->composing_range(), TextRange(4, 1)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } TEST(TextInputModel, MoveCursorToEndStart) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(0, 0)); + EXPECT_TRUE(model->SetSelection(TextRange(0))); EXPECT_TRUE(model->MoveCursorToEnd()); - EXPECT_EQ(model->selection_base(), 5); - EXPECT_EQ(model->selection_extent(), 5); + EXPECT_EQ(model->selection(), TextRange(5)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } TEST(TextInputModel, MoveCursorToEndMiddle) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(2, 2)); + EXPECT_TRUE(model->SetSelection(TextRange(2))); EXPECT_TRUE(model->MoveCursorToEnd()); - EXPECT_EQ(model->selection_base(), 5); - EXPECT_EQ(model->selection_extent(), 5); + EXPECT_EQ(model->selection(), TextRange(5)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } TEST(TextInputModel, MoveCursorToEndEnd) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(5, 5)); + EXPECT_TRUE(model->SetSelection(TextRange(5))); EXPECT_FALSE(model->MoveCursorToEnd()); - EXPECT_EQ(model->selection_base(), 5); - EXPECT_EQ(model->selection_extent(), 5); + EXPECT_EQ(model->selection(), TextRange(5)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } TEST(TextInputModel, MoveCursorToEndSelection) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(1, 4)); + EXPECT_TRUE(model->SetSelection(TextRange(1, 4))); EXPECT_TRUE(model->MoveCursorToEnd()); - EXPECT_EQ(model->selection_base(), 5); - EXPECT_EQ(model->selection_extent(), 5); + EXPECT_EQ(model->selection(), TextRange(5)); + EXPECT_EQ(model->composing_range(), TextRange(0)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } TEST(TextInputModel, MoveCursorToEndReverseSelection) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(4, 1)); + EXPECT_TRUE(model->SetSelection(TextRange(4, 1))); + EXPECT_TRUE(model->MoveCursorToEnd()); + EXPECT_EQ(model->selection(), TextRange(5)); + EXPECT_EQ(model->composing_range(), TextRange(0)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, MoveCursorToEndStartComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 0)); + EXPECT_TRUE(model->MoveCursorToEnd()); + EXPECT_EQ(model->selection(), TextRange(4)); + EXPECT_EQ(model->composing_range(), TextRange(1, 4)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, MoveCursorToEndStartReverseComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 0)); + EXPECT_TRUE(model->MoveCursorToEnd()); + EXPECT_EQ(model->selection(), TextRange(4)); + EXPECT_EQ(model->composing_range(), TextRange(1, 4)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, MoveCursorToEndMiddleComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 1)); EXPECT_TRUE(model->MoveCursorToEnd()); - EXPECT_EQ(model->selection_base(), 5); - EXPECT_EQ(model->selection_extent(), 5); + EXPECT_EQ(model->selection(), TextRange(4)); + EXPECT_EQ(model->composing_range(), TextRange(1, 4)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, MoveCursorToEndMiddleReverseComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(4, 1), 1)); + EXPECT_TRUE(model->MoveCursorToEnd()); + EXPECT_EQ(model->selection(), TextRange(4)); + EXPECT_EQ(model->composing_range(), TextRange(4, 1)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, MoveCursorToEndEndComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(1, 4), 3)); + EXPECT_FALSE(model->MoveCursorToEnd()); + EXPECT_EQ(model->selection(), TextRange(4)); + EXPECT_EQ(model->composing_range(), TextRange(1, 4)); + EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); +} + +TEST(TextInputModel, MoveCursorToEndEndReverseComposing) { + auto model = std::make_unique(); + model->SetText("ABCDE"); + model->BeginComposing(); + EXPECT_TRUE(model->SetComposingRange(TextRange(4, 1), 3)); + EXPECT_FALSE(model->MoveCursorToEnd()); + EXPECT_EQ(model->selection(), TextRange(4)); + EXPECT_EQ(model->composing_range(), TextRange(4, 1)); EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } @@ -658,7 +1460,7 @@ TEST(TextInputModel, GetCursorOffset) { auto model = std::make_unique(); // These characters take 1, 2, 3 and 4 bytes in UTF-8. model->SetText("$¢€𐍈"); - EXPECT_TRUE(model->SetSelection(0, 0)); + EXPECT_TRUE(model->SetSelection(TextRange(0))); EXPECT_EQ(model->GetCursorOffset(), 0); EXPECT_TRUE(model->MoveCursorForward()); EXPECT_EQ(model->GetCursorOffset(), 1); @@ -673,14 +1475,14 @@ TEST(TextInputModel, GetCursorOffset) { TEST(TextInputModel, GetCursorOffsetSelection) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(1, 4)); + EXPECT_TRUE(model->SetSelection(TextRange(1, 4))); EXPECT_EQ(model->GetCursorOffset(), 4); } TEST(TextInputModel, GetCursorOffsetReverseSelection) { auto model = std::make_unique(); model->SetText("ABCDE"); - EXPECT_TRUE(model->SetSelection(4, 1)); + EXPECT_TRUE(model->SetSelection(TextRange(4, 1))); EXPECT_EQ(model->GetCursorOffset(), 1); } diff --git a/shell/platform/common/cpp/text_range.h b/shell/platform/common/cpp/text_range.h new file mode 100644 index 0000000000000..d951c45f98e6e --- /dev/null +++ b/shell/platform/common/cpp/text_range.h @@ -0,0 +1,94 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include + +#include "flutter/fml/logging.h" + +// A directional range of text. +// +// A |TextRange| describes a range of text with |base| and |extent| positions. +// In the case where |base| == |extent|, the range is said to be collapsed, and +// when |base| > |extent|, the range is said to be reversed. +class TextRange { + public: + explicit TextRange(size_t position) : base_(position), extent_(position) {} + explicit TextRange(size_t base, size_t extent) + : base_(base), extent_(extent) {} + TextRange(const TextRange&) = default; + TextRange& operator=(const TextRange&) = default; + + virtual ~TextRange() = default; + + // The base position of the range. + size_t base() const { return base_; } + + // Sets the base position of the range. + void set_base(size_t pos) { base_ = pos; } + + // The extent position of the range. + size_t extent() const { return extent_; } + + // Sets the extent position of the range. + void set_extent(size_t pos) { extent_ = pos; } + + // The lesser of the base and extent positions. + size_t start() const { return std::min(base_, extent_); } + + // Sets the start position of the range. + void set_start(size_t pos) { + if (base_ <= extent_) { + base_ = pos; + } else { + extent_ = pos; + } + } + + // The greater of the base and extent positions. + size_t end() const { return std::max(base_, extent_); } + + // Sets the end position of the range. + void set_end(size_t pos) { + if (base_ <= extent_) { + extent_ = pos; + } else { + base_ = pos; + } + } + + // The position of a collapsed range. + // + // Asserts that the range is of length 0. + size_t position() const { + FML_DCHECK(base_ == extent_); + return extent_; + } + + // The length of the range. + size_t length() const { return end() - start(); } + + // Returns true if the range is of length 0. + bool collapsed() const { return base_ == extent_; } + + // Returns true if the base is greater than the extent. + bool reversed() const { return base_ > extent_; } + + // Returns true if |position| is contained within the range. + bool Contains(size_t position) const { + return position >= start() && position <= end(); + } + + // Returns true if |range| is contained within the range. + bool Contains(const TextRange& range) const { + return range.start() >= start() && range.end() <= end(); + } + + bool operator==(const TextRange& other) const { + return base_ == other.base_ && extent_ == other.extent_; + } + + private: + size_t base_; + size_t extent_; +}; diff --git a/shell/platform/common/cpp/text_range_unittests.cc b/shell/platform/common/cpp/text_range_unittests.cc new file mode 100644 index 0000000000000..9aecf34309b98 --- /dev/null +++ b/shell/platform/common/cpp/text_range_unittests.cc @@ -0,0 +1,248 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/shell/platform/common/cpp/text_range.h" + +#include "gtest/gtest.h" + +namespace flutter { + +TEST(TextRange, TextRangeFromPositionZero) { + TextRange range(0); + EXPECT_EQ(range.base(), size_t(0)); + EXPECT_EQ(range.extent(), size_t(0)); + EXPECT_EQ(range.start(), size_t(0)); + EXPECT_EQ(range.end(), size_t(0)); + EXPECT_EQ(range.length(), size_t(0)); + EXPECT_EQ(range.position(), size_t(0)); + EXPECT_TRUE(range.collapsed()); +} + +TEST(TextRange, TextRangeFromPositionNonZero) { + TextRange range(3); + EXPECT_EQ(range.base(), size_t(3)); + EXPECT_EQ(range.extent(), size_t(3)); + EXPECT_EQ(range.start(), size_t(3)); + EXPECT_EQ(range.end(), size_t(3)); + EXPECT_EQ(range.length(), size_t(0)); + EXPECT_EQ(range.position(), size_t(3)); + EXPECT_TRUE(range.collapsed()); +} + +TEST(TextRange, TextRangeFromRange) { + TextRange range(3, 7); + EXPECT_EQ(range.base(), size_t(3)); + EXPECT_EQ(range.extent(), size_t(7)); + EXPECT_EQ(range.start(), size_t(3)); + EXPECT_EQ(range.end(), size_t(7)); + EXPECT_EQ(range.length(), size_t(4)); + EXPECT_FALSE(range.collapsed()); +} + +TEST(TextRange, TextRangeFromReversedRange) { + TextRange range(7, 3); + EXPECT_EQ(range.base(), size_t(7)); + EXPECT_EQ(range.extent(), size_t(3)); + EXPECT_EQ(range.start(), size_t(3)); + EXPECT_EQ(range.end(), size_t(7)); + EXPECT_EQ(range.length(), size_t(4)); + EXPECT_FALSE(range.collapsed()); +} + +TEST(TextRange, SetBase) { + TextRange range(3, 7); + range.set_base(4); + EXPECT_EQ(range.base(), size_t(4)); + EXPECT_EQ(range.extent(), size_t(7)); +} + +TEST(TextRange, SetBaseReversed) { + TextRange range(7, 3); + range.set_base(5); + EXPECT_EQ(range.base(), size_t(5)); + EXPECT_EQ(range.extent(), size_t(3)); +} + +TEST(TextRange, SetExtent) { + TextRange range(3, 7); + range.set_extent(6); + EXPECT_EQ(range.base(), size_t(3)); + EXPECT_EQ(range.extent(), size_t(6)); +} + +TEST(TextRange, SetExtentReversed) { + TextRange range(7, 3); + range.set_extent(4); + EXPECT_EQ(range.base(), size_t(7)); + EXPECT_EQ(range.extent(), size_t(4)); +} + +TEST(TextRange, SetStart) { + TextRange range(3, 7); + range.set_start(5); + EXPECT_EQ(range.base(), size_t(5)); + EXPECT_EQ(range.extent(), size_t(7)); +} + +TEST(TextRange, SetStartReversed) { + TextRange range(7, 3); + range.set_start(5); + EXPECT_EQ(range.base(), size_t(7)); + EXPECT_EQ(range.extent(), size_t(5)); +} + +TEST(TextRange, SetEnd) { + TextRange range(3, 7); + range.set_end(6); + EXPECT_EQ(range.base(), size_t(3)); + EXPECT_EQ(range.extent(), size_t(6)); +} + +TEST(TextRange, SetEndReversed) { + TextRange range(7, 3); + range.set_end(5); + EXPECT_EQ(range.base(), size_t(5)); + EXPECT_EQ(range.extent(), size_t(3)); +} + +TEST(TextRange, ContainsPreStartPosition) { + TextRange range(2, 6); + EXPECT_FALSE(range.Contains(1)); +} + +TEST(TextRange, ContainsStartPosition) { + TextRange range(2, 6); + EXPECT_TRUE(range.Contains(2)); +} + +TEST(TextRange, ContainsMiddlePosition) { + TextRange range(2, 6); + EXPECT_TRUE(range.Contains(3)); + EXPECT_TRUE(range.Contains(4)); +} + +TEST(TextRange, ContainsEndPosition) { + TextRange range(2, 6); + EXPECT_TRUE(range.Contains(6)); +} + +TEST(TextRange, ContainsPostEndPosition) { + TextRange range(2, 6); + EXPECT_FALSE(range.Contains(7)); +} + +TEST(TextRange, ContainsPreStartPositionReversed) { + TextRange range(6, 2); + EXPECT_FALSE(range.Contains(1)); +} + +TEST(TextRange, ContainsStartPositionReversed) { + TextRange range(6, 2); + EXPECT_TRUE(range.Contains(2)); +} + +TEST(TextRange, ContainsMiddlePositionReversed) { + TextRange range(6, 2); + EXPECT_TRUE(range.Contains(3)); + EXPECT_TRUE(range.Contains(4)); +} + +TEST(TextRange, ContainsEndPositionReversed) { + TextRange range(6, 2); + EXPECT_TRUE(range.Contains(6)); +} + +TEST(TextRange, ContainsPostEndPositionReversed) { + TextRange range(6, 2); + EXPECT_FALSE(range.Contains(7)); +} + +TEST(TextRange, ContainsRangePreStartPosition) { + TextRange range(2, 6); + EXPECT_FALSE(range.Contains(TextRange(0, 1))); +} + +TEST(TextRange, ContainsRangeSpanningStartPosition) { + TextRange range(2, 6); + EXPECT_FALSE(range.Contains(TextRange(1, 3))); +} + +TEST(TextRange, ContainsRangeStartPosition) { + TextRange range(2, 6); + EXPECT_TRUE(range.Contains(TextRange(2))); +} + +TEST(TextRange, ContainsRangeMiddlePosition) { + TextRange range(2, 6); + EXPECT_TRUE(range.Contains(TextRange(3, 4))); + EXPECT_TRUE(range.Contains(TextRange(4, 5))); +} + +TEST(TextRange, ContainsRangeEndPosition) { + TextRange range(2, 6); + EXPECT_TRUE(range.Contains(TextRange(6))); +} + +TEST(TextRange, ContainsRangeSpanningEndPosition) { + TextRange range(2, 6); + EXPECT_FALSE(range.Contains(TextRange(5, 7))); +} + +TEST(TextRange, ContainsRangePostEndPosition) { + TextRange range(2, 6); + EXPECT_FALSE(range.Contains(TextRange(6, 7))); +} + +TEST(TextRange, ContainsRangePreStartPositionReversed) { + TextRange range(6, 2); + EXPECT_FALSE(range.Contains(TextRange(0, 1))); +} + +TEST(TextRange, ContainsRangeSpanningStartPositionReversed) { + TextRange range(6, 2); + EXPECT_FALSE(range.Contains(TextRange(1, 3))); +} + +TEST(TextRange, ContainsRangeStartPositionReversed) { + TextRange range(6, 2); + EXPECT_TRUE(range.Contains(TextRange(2))); +} + +TEST(TextRange, ContainsRangeMiddlePositionReversed) { + TextRange range(6, 2); + EXPECT_TRUE(range.Contains(TextRange(3, 4))); + EXPECT_TRUE(range.Contains(TextRange(4, 5))); +} + +TEST(TextRange, ContainsRangeSpanningEndPositionReversed) { + TextRange range(6, 2); + EXPECT_FALSE(range.Contains(TextRange(5, 7))); +} + +TEST(TextRange, ContainsRangeEndPositionReversed) { + TextRange range(6, 2); + EXPECT_TRUE(range.Contains(TextRange(5))); +} + +TEST(TextRange, ContainsRangePostEndPositionReversed) { + TextRange range(6, 2); + EXPECT_FALSE(range.Contains(TextRange(6, 7))); +} + +TEST(TextRange, ReversedForwardRange) { + TextRange range(2, 6); + EXPECT_FALSE(range.reversed()); +} + +TEST(TextRange, ReversedCollapsedRange) { + TextRange range(2, 2); + EXPECT_FALSE(range.reversed()); +} + +TEST(TextRange, ReversedReversedRange) { + TextRange range(6, 2); + EXPECT_TRUE(range.reversed()); +} + +} // namespace flutter diff --git a/shell/platform/darwin/common/framework/Source/FlutterChannels.mm b/shell/platform/darwin/common/framework/Source/FlutterChannels.mm index d225a2ee8d2b0..f5527a438ed9e 100644 --- a/shell/platform/darwin/common/framework/Source/FlutterChannels.mm +++ b/shell/platform/darwin/common/framework/Source/FlutterChannels.mm @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h" diff --git a/shell/platform/darwin/common/framework/Source/FlutterCodecs.mm b/shell/platform/darwin/common/framework/Source/FlutterCodecs.mm index 3f5379165cf00..a73ede961e1ac 100644 --- a/shell/platform/darwin/common/framework/Source/FlutterCodecs.mm +++ b/shell/platform/darwin/common/framework/Source/FlutterCodecs.mm @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h" diff --git a/shell/platform/darwin/common/framework/Source/FlutterStandardCodec.mm b/shell/platform/darwin/common/framework/Source/FlutterStandardCodec.mm index a76ec3672e787..984dfff8435b4 100644 --- a/shell/platform/darwin/common/framework/Source/FlutterStandardCodec.mm +++ b/shell/platform/darwin/common/framework/Source/FlutterStandardCodec.mm @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #import "flutter/shell/platform/darwin/common/framework/Source/FlutterStandardCodec_Internal.h" diff --git a/shell/platform/darwin/common/framework/Source/flutter_standard_codec_unittest.mm b/shell/platform/darwin/common/framework/Source/flutter_standard_codec_unittest.mm index 5095dc273efcc..91ffd60935bc9 100644 --- a/shell/platform/darwin/common/framework/Source/flutter_standard_codec_unittest.mm +++ b/shell/platform/darwin/common/framework/Source/flutter_standard_codec_unittest.mm @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h" diff --git a/shell/platform/darwin/ios/BUILD.gn b/shell/platform/darwin/ios/BUILD.gn index 9bf404552877f..793c97be93701 100644 --- a/shell/platform/darwin/ios/BUILD.gn +++ b/shell/platform/darwin/ios/BUILD.gn @@ -93,6 +93,8 @@ source_set("flutter_framework_source") { "ios_context_software.mm", "ios_external_texture_gl.h", "ios_external_texture_gl.mm", + "ios_external_view_embedder.h", + "ios_external_view_embedder.mm", "ios_render_target_gl.h", "ios_render_target_gl.mm", "ios_surface.h", @@ -158,7 +160,6 @@ source_set("flutter_framework_source") { } } -ios_test_flutter_path = rebase_path("$root_out_dir/libios_test_flutter.dylib") platform_frameworks_path = rebase_path("$ios_sdk_path/../../Library/Frameworks/") @@ -181,15 +182,12 @@ source_set("ios_test_flutter_mrc") { "//flutter/shell/platform/darwin/common:framework_shared", "//flutter/third_party/tonic", "//flutter/third_party/txt", - "//third_party/ocmock:ocmock", + "//third_party/ocmock:ocmock_shared", "//third_party/rapidjson", "//third_party/skia", ] } -# NOTE: This currently only supports simulator targets because of the install_name. -# TODO(54504): Switch the install_name and make the test runner copy the dynamic -# library into the testing bundle. shared_library("ios_test_flutter") { visibility = [ ":*" ] cflags = [ @@ -200,9 +198,9 @@ shared_library("ios_test_flutter") { ] ldflags = [ "-F$platform_frameworks_path", - "-Wl,-framework,XCTest", - "-Wl,-install_name,$ios_test_flutter_path", + "-Wl,-install_name,@rpath/Frameworks/libios_test_flutter.dylib", ] + libs = [ "XCTest.framework" ] configs -= [ "//build/config/gcc:symbol_visibility_hidden", "//build/config:symbol_visibility_hidden", @@ -223,7 +221,7 @@ shared_library("ios_test_flutter") { "//flutter/shell/platform/darwin/common:framework_shared", "//flutter/third_party/tonic", "//flutter/third_party/txt", - "//third_party/ocmock:ocmock", + "//third_party/ocmock:ocmock_shared", "//third_party/rapidjson", "//third_party/skia", ] diff --git a/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h b/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h index 98ccb8354896e..7583a8d7aa04d 100644 --- a/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h +++ b/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h @@ -322,6 +322,14 @@ FLUTTER_EXPORT */ @property(nonatomic, readonly) FlutterBasicMessageChannel* settingsChannel; +/** + * The `FlutterBasicMessageChannel` used for communicating key events + * from physical keyboards + * + * Can be nil after `destroyContext` is called. + */ +@property(nonatomic, readonly) FlutterBasicMessageChannel* keyEventChannel; + /** * The `NSURL` of the observatory for the service isolate. * diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm index 347b853270e89..cbd93e978fd6a 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm @@ -78,6 +78,7 @@ @implementation FlutterEngine { fml::scoped_nsobject _lifecycleChannel; fml::scoped_nsobject _systemChannel; fml::scoped_nsobject _settingsChannel; + fml::scoped_nsobject _keyEventChannel; int64_t _nextTextureId; @@ -337,6 +338,9 @@ - (FlutterBasicMessageChannel*)systemChannel { - (FlutterBasicMessageChannel*)settingsChannel { return _settingsChannel.get(); } +- (FlutterBasicMessageChannel*)keyEventChannel { + return _keyEventChannel.get(); +} - (NSURL*)observatoryUrl { return [_publisher.get() url]; @@ -351,6 +355,7 @@ - (void)resetChannels { _lifecycleChannel.reset(); _systemChannel.reset(); _settingsChannel.reset(); + _keyEventChannel.reset(); } - (void)startProfiler:(NSString*)threadLabel { @@ -423,6 +428,11 @@ - (void)setupChannels { binaryMessenger:self.binaryMessenger codec:[FlutterJSONMessageCodec sharedInstance]]); + _keyEventChannel.reset([[FlutterBasicMessageChannel alloc] + initWithName:@"flutter/keyevent" + binaryMessenger:self.binaryMessenger + codec:[FlutterJSONMessageCodec sharedInstance]]); + _textInputPlugin.reset([[FlutterTextInputPlugin alloc] init]); _textInputPlugin.get().textInputDelegate = self; @@ -474,6 +484,8 @@ - (BOOL)createShell:(NSString*)entrypoint self.initialRoute = initialRoute; auto settings = [_dartProject.get() settings]; + FlutterView.forceSoftwareRendering = settings.enable_software_rendering; + auto platformData = [_dartProject.get() defaultPlatformData]; if (libraryURI) { @@ -513,7 +525,8 @@ - (BOOL)createShell:(NSString*)entrypoint flutter::Shell::CreateCallback on_create_platform_view = [](flutter::Shell& shell) { return std::make_unique( - shell, flutter::GetRenderingAPIForProcess(), shell.GetTaskRunners()); + shell, flutter::GetRenderingAPIForProcess(FlutterView.forceSoftwareRendering), + shell.GetTaskRunners()); }; flutter::Shell::CreateCallback on_create_rasterizer = @@ -544,7 +557,8 @@ - (BOOL)createShell:(NSString*)entrypoint if (!_platformViewsController) { _platformViewsController.reset(new flutter::FlutterPlatformViewsController()); } - _publisher.reset([[FlutterObservatoryPublisher alloc] init]); + _publisher.reset([[FlutterObservatoryPublisher alloc] + initWithEnableObservatoryPublication:settings.enable_observatory_publication]); [self maybeSetupPlatformViewChannels]; _shell->GetIsGpuDisabledSyncSwitch()->SetSwitch(_isGpuDisabled ? true : false); if (profilerEnabled) { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.h b/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.h index 387176f76e25e..a00c17ad89245 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.h @@ -9,6 +9,11 @@ @interface FlutterObservatoryPublisher : NSObject +- (instancetype)initWithEnableObservatoryPublication:(BOOL)enableObservatoryPublication + NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; + @property(nonatomic, readonly) NSURL* url; @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.mm b/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.mm index 072a244a967fc..c9b124ba4e264 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.mm @@ -9,6 +9,9 @@ #if FLUTTER_RELEASE @implementation FlutterObservatoryPublisher +- (instancetype)initWithEnableObservatoryPublication:(BOOL)enableObservatoryPublication { + return [super init]; +} @end #else // FLUTTER_RELEASE @@ -37,26 +40,23 @@ @implementation FlutterObservatoryPublisher #include #include "flutter/fml/logging.h" -#include "flutter/fml/make_copyable.h" #include "flutter/fml/memory/weak_ptr.h" #include "flutter/fml/message_loop.h" #include "flutter/fml/platform/darwin/scoped_nsobject.h" -#include "flutter/fml/task_runner.h" #include "flutter/runtime/dart_service_isolate.h" @protocol FlutterObservatoryPublisherDelegate -- (instancetype)initWithOwner:(FlutterObservatoryPublisher*)owner; -- (void)publishServiceProtocolPort:(NSString*)uri; +- (void)publishServiceProtocolPort:(NSURL*)uri; - (void)stopService; - -@property(readonly) fml::scoped_nsobject url; @end @interface FlutterObservatoryPublisher () -- (NSData*)createTxtData:(NSURL*)url; ++ (NSData*)createTxtData:(NSURL*)url; -@property(readonly) NSString* serviceName; +@property(readonly, class) NSString* serviceName; @property(readonly) fml::scoped_nsobject> delegate; +@property(nonatomic, readwrite) NSURL* url; +@property(readonly) BOOL enableObservatoryPublication; @end @@ -68,19 +68,9 @@ @interface ObservatoryDNSServiceDelegate : NSObject _owner; DNSServiceRef _dnsServiceRef; } -@synthesize url; - -- (instancetype)initWithOwner:(FlutterObservatoryPublisher*)owner { - self = [super init]; - NSAssert(self, @"Super must not return null on init."); - _owner.reset([owner retain]); - return self; -} - - (void)stopService { if (_dnsServiceRef) { DNSServiceRefDeallocate(_dnsServiceRef); @@ -88,11 +78,7 @@ - (void)stopService { } } -- (void)publishServiceProtocolPort:(NSString*)uri { - // uri comes in as something like 'http://127.0.0.1:XXXXX/' where XXXXX is the port - // number. - url.reset([[NSURL alloc] initWithString:uri]); - +- (void)publishServiceProtocolPort:(NSURL*)url { DNSServiceFlags flags = kDNSServiceFlagsDefault; #if TARGET_IPHONE_SIMULATOR // Simulator needs to use local loopback explicitly to work. @@ -105,11 +91,11 @@ - (void)publishServiceProtocolPort:(NSString*)uri { const char* domain = "local."; // default domain uint16_t port = [[url port] unsignedShortValue]; - NSData* txtData = [_owner createTxtData:url.get()]; - int err = - DNSServiceRegister(&_dnsServiceRef, flags, interfaceIndex, - [_owner.get().serviceName UTF8String], registrationType, domain, NULL, - htons(port), txtData.length, txtData.bytes, registrationCallback, NULL); + NSData* txtData = [FlutterObservatoryPublisher createTxtData:url]; + int err = DNSServiceRegister(&_dnsServiceRef, flags, interfaceIndex, + FlutterObservatoryPublisher.serviceName.UTF8String, registrationType, + domain, NULL, htons(port), txtData.length, txtData.bytes, + registrationCallback, NULL); if (err != 0) { FML_LOG(ERROR) << "Failed to register observatory port with mDNS with error " << err << "."; @@ -122,8 +108,8 @@ - (void)publishServiceProtocolPort:(NSString*)uri { << "to the 'NSBonjourServices' key in your Info.plist for the Debug/" << "Profile configurations. " << "For more information, see " - // Update link to a specific header as needed. - << "https://flutter.dev/docs/development/add-to-app/ios/project-setup"; + << "https://flutter.dev/docs/development/add-to-app/ios/" + "project-setup#local-network-privacy-permissions"; } } else { DNSServiceSetDispatchQueue(_dnsServiceRef, dispatch_get_main_queue()); @@ -162,34 +148,21 @@ static void DNSSD_API registrationCallback(DNSServiceRef sdRef, @end @implementation ObservatoryNSNetServiceDelegate { - fml::scoped_nsobject _owner; fml::scoped_nsobject _netService; } -@synthesize url; - -- (instancetype)initWithOwner:(FlutterObservatoryPublisher*)owner { - self = [super init]; - NSAssert(self, @"Super must not return null on init."); - _owner.reset([owner retain]); - return self; -} - - (void)stopService { [_netService.get() stop]; [_netService.get() setDelegate:nil]; } -- (void)publishServiceProtocolPort:(NSString*)uri { - // uri comes in as something like 'http://127.0.0.1:XXXXX/' where XXXXX is the port - // number. - url.reset([[NSURL alloc] initWithString:uri]); - - NSNetService* netServiceTmp = [[NSNetService alloc] initWithDomain:@"local." - type:@"_dartobservatory._tcp." - name:_owner.get().serviceName - port:[[url port] intValue]]; - [netServiceTmp setTXTRecordData:[_owner createTxtData:url.get()]]; +- (void)publishServiceProtocolPort:(NSURL*)url { + NSNetService* netServiceTmp = + [[NSNetService alloc] initWithDomain:@"local." + type:@"_dartobservatory._tcp." + name:FlutterObservatoryPublisher.serviceName + port:[[url port] intValue]]; + [netServiceTmp setTXTRecordData:[FlutterObservatoryPublisher createTxtData:url]]; _netService.reset(netServiceTmp); [_netService.get() setDelegate:self]; [_netService.get() publish]; @@ -211,19 +184,16 @@ @implementation FlutterObservatoryPublisher { std::unique_ptr> _weakFactory; } -- (NSURL*)url { - return [_delegate.get().url autorelease]; -} - -- (instancetype)init { +- (instancetype)initWithEnableObservatoryPublication:(BOOL)enableObservatoryPublication { self = [super init]; NSAssert(self, @"Super must not return null on init."); if (@available(iOS 9.3, *)) { - _delegate.reset([[ObservatoryDNSServiceDelegate alloc] initWithOwner:self]); + _delegate.reset([[ObservatoryDNSServiceDelegate alloc] init]); } else { - _delegate.reset([[ObservatoryNSNetServiceDelegate alloc] initWithOwner:self]); + _delegate.reset([[ObservatoryNSNetServiceDelegate alloc] init]); } + _enableObservatoryPublication = enableObservatoryPublication; _weakFactory = std::make_unique>(self); fml::MessageLoop::EnsureInitializedForCurrentThread(); @@ -233,9 +203,15 @@ - (instancetype)init { runner = fml::MessageLoop::GetCurrent().GetTaskRunner()](const std::string& uri) { if (!uri.empty()) { runner->PostTask([weak, uri]() { + // uri comes in as something like 'http://127.0.0.1:XXXXX/' where XXXXX is the port + // number. if (weak) { - [[weak.get() delegate] - publishServiceProtocolPort:[NSString stringWithUTF8String:uri.c_str()]]; + NSURL* url = + [[NSURL alloc] initWithString:[NSString stringWithUTF8String:uri.c_str()]]; + weak.get().url = url; + if (weak.get().enableObservatoryPublication) { + [[weak.get() delegate] publishServiceProtocolPort:url]; + } } }); } @@ -244,11 +220,11 @@ - (instancetype)init { return self; } -- (NSString*)serviceName { ++ (NSString*)serviceName { return NSBundle.mainBundle.bundleIdentifier; } -- (NSData*)createTxtData:(NSURL*)url { ++ (NSData*)createTxtData:(NSURL*)url { // Check to see if there's an authentication code. If there is, we'll provide // it as a txt record so flutter tools can establish a connection. NSString* path = [[url path] substringFromIndex:MIN(1, [[url path] length])]; @@ -261,6 +237,7 @@ - (NSData*)createTxtData:(NSURL*)url { - (void)dealloc { [_delegate stopService]; + [_url release]; flutter::DartServiceIsolate::RemoveServerStatusCallback(std::move(_callbackHandle)); [super dealloc]; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegateTest.m b/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegateTest.m index da2cc200781c9..b5d9c153fef6f 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegateTest.m +++ b/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegateTest.m @@ -51,7 +51,7 @@ - (void)testWillResignActive { OCMVerify([plugin applicationWillResignActive:[UIApplication sharedApplication]]); } -- (void)testDidBecomeActive { +- (void)skip_testDidBecomeActive { FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init]; id plugin = OCMProtocolMock(@protocol(FlutterPlugin)); [delegate addDelegate:plugin]; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterView.h b/shell/platform/darwin/ios/framework/Source/FlutterView.h index 2806c3359f6c3..3a9c138de02aa 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterView.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterView.h @@ -21,7 +21,6 @@ asBase64Encoded:(BOOL)base64Encode; - (flutter::FlutterPlatformViewsController*)platformViewsController; - @end @interface FlutterView : UIView @@ -35,6 +34,8 @@ opaque:(BOOL)opaque NS_DESIGNATED_INITIALIZER; - (std::unique_ptr)createSurface:(std::shared_ptr)context; +// Set by FlutterEngine or FlutterViewController to override software rendering. +@property(class, nonatomic) BOOL forceSoftwareRendering; @end #endif // SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTER_VIEW_H_ diff --git a/shell/platform/darwin/ios/framework/Source/FlutterView.mm b/shell/platform/darwin/ios/framework/Source/FlutterView.mm index 8fb97f68335ef..3f6b19decdac3 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterView.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterView.mm @@ -68,8 +68,19 @@ - (void)layoutSubviews { [super layoutSubviews]; } +static BOOL _forceSoftwareRendering; + ++ (BOOL)forceSoftwareRendering { + return _forceSoftwareRendering; +} + ++ (void)setForceSoftwareRendering:(BOOL)forceSoftwareRendering { + _forceSoftwareRendering = forceSoftwareRendering; +} + + (Class)layerClass { - return flutter::GetCoreAnimationLayerClassForRenderingAPI(); + return flutter::GetCoreAnimationLayerClassForRenderingAPI( + flutter::GetRenderingAPIForProcess(FlutterView.forceSoftwareRendering)); } - (std::unique_ptr)createSurface: diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 792256082fbf7..0b88a1ed543d9 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -157,6 +157,12 @@ - (instancetype)init { - (void)sharedSetupWithProject:(nullable FlutterDartProject*)project initialRoute:(nullable NSString*)initialRoute { + // Need the project to get settings for the view. Initializing it here means + // the Engine class won't initialize it later. + if (!project) { + project = [[[FlutterDartProject alloc] init] autorelease]; + } + FlutterView.forceSoftwareRendering = project.settings.enable_software_rendering; auto engine = fml::scoped_nsobject{[[FlutterEngine alloc] initWithName:@"io.flutter" project:project @@ -1018,6 +1024,58 @@ - (void)keyboardWillBeHidden:(NSNotification*)notification { [self updateViewportMetrics]; } +- (void)dispatchPresses:(NSSet*)presses API_AVAILABLE(ios(13.4)) { + if (@available(iOS 13.4, *)) { + for (UIPress* press in presses) { + if (press.key == nil || press.phase == UIPressPhaseStationary || + press.phase == UIPressPhaseChanged) { + continue; + } + NSMutableDictionary* keyMessage = [[@{ + @"keymap" : @"ios", + @"type" : @"unknown", + @"keyCode" : @(press.key.keyCode), + @"modifiers" : @(press.key.modifierFlags), + @"characters" : press.key.characters, + @"charactersIgnoringModifiers" : press.key.charactersIgnoringModifiers + } mutableCopy] autorelease]; + + if (press.phase == UIPressPhaseBegan) { + keyMessage[@"type"] = @"keydown"; + } else if (press.phase == UIPressPhaseEnded || press.phase == UIPressPhaseCancelled) { + keyMessage[@"type"] = @"keyup"; + } + + [[_engine.get() keyEventChannel] sendMessage:keyMessage]; + } + } +} + +- (void)pressesBegan:(NSSet*)presses withEvent:(UIEvent*)event API_AVAILABLE(ios(9.0)) { + if (@available(iOS 13.4, *)) { + [self dispatchPresses:presses]; + } +} + +- (void)pressesChanged:(NSSet*)presses withEvent:(UIEvent*)event API_AVAILABLE(ios(9.0)) { + if (@available(iOS 13.4, *)) { + [self dispatchPresses:presses]; + } +} + +- (void)pressesEnded:(NSSet*)presses withEvent:(UIEvent*)event API_AVAILABLE(ios(9.0)) { + if (@available(iOS 13.4, *)) { + [self dispatchPresses:presses]; + } +} + +- (void)pressesCancelled:(NSSet*)presses + withEvent:(UIEvent*)event API_AVAILABLE(ios(9.0)) { + if (@available(iOS 13.4, *)) { + [self dispatchPresses:presses]; + } +} + #pragma mark - Orientation updates - (void)onOrientationPreferencesUpdated:(NSNotification*)notification { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm index f15ea37666319..9f4348dd6812f 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm @@ -62,6 +62,7 @@ - (UIAccessibilityContrast)accessibilityContrast; @interface FlutterViewController (Tests) - (void)surfaceUpdated:(BOOL)appeared; - (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences; +- (void)dispatchPresses:(NSSet*)presses; @end @implementation FlutterViewControllerTest @@ -549,4 +550,150 @@ - (void)testNotifyLowMemory { OCMVerify([engine notifyLowMemory]); } +- (void)testValidKeyUpEvent API_AVAILABLE(ios(13.4)) { + if (@available(iOS 13.4, *)) { + // noop + } else { + return; + } + + id engine = OCMClassMock([FlutterEngine class]); + + id keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]); + OCMStub([engine keyEventChannel]).andReturn(keyEventChannel); + + FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; + + id testSet = [self fakeUiPressSetForPhase:UIPressPhaseBegan + keyCode:UIKeyboardHIDUsageKeyboardA + modifierFlags:UIKeyModifierShift + characters:@"a" + charactersIgnoringModifiers:@"A"]; + + // Exercise behavior under test. + [vc dispatchPresses:testSet]; + + // Verify behavior. + OCMVerify([keyEventChannel + sendMessage:[OCMArg checkWithBlock:^BOOL(id message) { + return [message[@"keymap"] isEqualToString:@"ios"] && + [message[@"type"] isEqualToString:@"keydown"] && + [message[@"keyCode"] isEqualToNumber:[NSNumber numberWithInt:4]] && + [message[@"modifiers"] isEqualToNumber:[NSNumber numberWithInt:131072]] && + [message[@"characters"] isEqualToString:@"a"] && + [message[@"charactersIgnoringModifiers"] isEqualToString:@"A"]; + }]]); + + // Clean up mocks + [engine stopMocking]; + [keyEventChannel stopMocking]; +} + +- (void)testValidKeyDownEvent API_AVAILABLE(ios(13.4)) { + if (@available(iOS 13.4, *)) { + // noop + } else { + return; + } + + id engine = OCMClassMock([FlutterEngine class]); + + id keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]); + OCMStub([engine keyEventChannel]).andReturn(keyEventChannel); + + FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; + + id testSet = [self fakeUiPressSetForPhase:UIPressPhaseEnded + keyCode:UIKeyboardHIDUsageKeyboardA + modifierFlags:UIKeyModifierShift + characters:@"a" + charactersIgnoringModifiers:@"A"]; + + // Exercise behavior under test. + [vc dispatchPresses:testSet]; + + // Verify behavior. + OCMVerify([keyEventChannel + sendMessage:[OCMArg checkWithBlock:^BOOL(id message) { + return [message[@"keymap"] isEqualToString:@"ios"] && + [message[@"type"] isEqualToString:@"keyup"] && + [message[@"keyCode"] isEqualToNumber:[NSNumber numberWithInt:4]] && + [message[@"modifiers"] isEqualToNumber:[NSNumber numberWithInt:131072]] && + [message[@"characters"] isEqualToString:@"a"] && + [message[@"charactersIgnoringModifiers"] isEqualToString:@"A"]; + }]]); + + // Clean up mocks + [engine stopMocking]; + [keyEventChannel stopMocking]; +} + +- (void)testIgnoredKeyEvents API_AVAILABLE(ios(13.4)) { + if (@available(iOS 13.4, *)) { + // noop + } else { + return; + } + + id engine = OCMClassMock([FlutterEngine class]); + + id keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]); + OCMStub([engine keyEventChannel]).andReturn(keyEventChannel); + + FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; + + id emptySet = [NSSet set]; + id ignoredSet = [self fakeUiPressSetForPhase:UIPressPhaseStationary + keyCode:UIKeyboardHIDUsageKeyboardA + modifierFlags:UIKeyModifierShift + characters:@"a" + charactersIgnoringModifiers:@"A"]; + + id mockUiPress = OCMClassMock([UIPress class]); + OCMStub([mockUiPress phase]).andReturn(UIPressPhaseBegan); + id emptyKeySet = [NSSet setWithArray:@[ mockUiPress ]]; + // Exercise behavior under test. + [vc dispatchPresses:emptySet]; + [vc dispatchPresses:ignoredSet]; + [vc dispatchPresses:emptyKeySet]; + + // Verify behavior. + OCMVerify(never(), [keyEventChannel sendMessage:[OCMArg any]]); + + // Clean up mocks + [engine stopMocking]; + [keyEventChannel stopMocking]; +} + +- (NSSet*)fakeUiPressSetForPhase:(UIPressPhase)phase + keyCode:(UIKeyboardHIDUsage)keyCode + modifierFlags:(UIKeyModifierFlags)modifierFlags + characters:(NSString*)characters + charactersIgnoringModifiers:(NSString*)charactersIgnoringModifiers + API_AVAILABLE(ios(13.4)) { + if (@available(iOS 13.4, *)) { + // noop + } else { + return [NSSet set]; + } + id mockUiPress = OCMClassMock([UIPress class]); + OCMStub([mockUiPress phase]).andReturn(phase); + + id mockUiKey = OCMClassMock([UIKey class]); + OCMStub([mockUiKey keyCode]).andReturn(keyCode); + OCMStub([mockUiKey modifierFlags]).andReturn(modifierFlags); + OCMStub([mockUiKey characters]).andReturn(characters); + OCMStub([mockUiKey charactersIgnoringModifiers]).andReturn(charactersIgnoringModifiers); + + OCMStub([mockUiPress key]).andReturn(mockUiKey); + + return [NSSet setWithArray:@[ mockUiPress ]]; +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/SemanticsObject.h b/shell/platform/darwin/ios/framework/Source/SemanticsObject.h index dded845d70137..8df516483f270 100644 --- a/shell/platform/darwin/ios/framework/Source/SemanticsObject.h +++ b/shell/platform/darwin/ios/framework/Source/SemanticsObject.h @@ -93,7 +93,7 @@ constexpr int32_t kRootNodeId = 0; - (BOOL)nodeWillCauseScroll:(const flutter::SemanticsNode*)node; - (BOOL)nodeShouldTriggerAnnouncement:(const flutter::SemanticsNode*)node; - (void)collectRoutes:(NSMutableArray*)edges; -- (SemanticsObject*)routeFocusObject; +- (NSString*)routeName; - (BOOL)onCustomAccessibilityAction:(FlutterCustomAccessibilityAction*)action; @end diff --git a/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm b/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm index 546e9c5c2c727..d41f0a40aee6f 100644 --- a/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm +++ b/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm @@ -282,21 +282,20 @@ - (BOOL)onCustomAccessibilityAction:(FlutterCustomAccessibilityAction*)action { return YES; } -- (SemanticsObject*)routeFocusObject { - // Returns the first SemanticObject in this branch that has - // the NamesRoute flag with a non-nil semantic label. Otherwise - // returns nil. +- (NSString*)routeName { + // Returns the first non-null and non-empty semantic label of a child + // with an NamesRoute flag. Otherwise returns nil. if ([self node].HasFlag(flutter::SemanticsFlags::kNamesRoute)) { NSString* newName = [self accessibilityLabel]; if (newName != nil && [newName length] > 0) { - return self; + return newName; } } if ([self hasChildren]) { for (SemanticsObject* child in self.children) { - SemanticsObject* focusObject = [child routeFocusObject]; - if (focusObject != nil) { - return focusObject; + NSString* newName = [child routeName]; + if (newName != nil && [newName length] > 0) { + return newName; } } } diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm index bad9aca0f1e29..33ea01d1881fa 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm @@ -198,12 +198,9 @@ void PostAccessibilityNotification(UIAccessibilityNotifications notification, // We should send out only one notification per semantics update. if (routeChanged) { if (!ios_delegate_->IsFlutterViewControllerPresentingModalViewController(view_controller_)) { - SemanticsObject* nextToFocus = [lastAdded routeFocusObject]; - if (!nextToFocus && root) { - nextToFocus = FindFirstFocusable(root); - } + NSString* routeName = [lastAdded routeName]; ios_delegate_->PostAccessibilityNotification(UIAccessibilityScreenChangedNotification, - nextToFocus); + routeName); } } else if (layoutChanged) { SemanticsObject* nextToFocus = nil; diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm b/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm index 7f73bc0a5cf22..41970e67c8f73 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm @@ -328,9 +328,7 @@ - (void)testAnnouncesRouteChanges { bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions); XCTAssertEqual([accessibility_notifications count], 1ul); - SemanticsObject* focusObject = accessibility_notifications[0][@"argument"]; - XCTAssertEqual([focusObject uid], 3); - XCTAssertEqualObjects([focusObject accessibilityLabel], @"node3"); + XCTAssertEqualObjects(accessibility_notifications[0][@"argument"], @"node3"); XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue], UIAccessibilityScreenChangedNotification); } @@ -373,7 +371,8 @@ - (void)testAnnouncesRouteChangesWhenNoNamesRoute { flutter::SemanticsNode node1; node1.id = 1; node1.label = "node1"; - node1.flags = static_cast(flutter::SemanticsFlags::kScopesRoute); + node1.flags = static_cast(flutter::SemanticsFlags::kScopesRoute) | + static_cast(flutter::SemanticsFlags::kNamesRoute); node1.childrenInTraversalOrder = {2, 3}; node1.childrenInHitTestOrder = {2, 3}; nodes[node1.id] = node1; @@ -394,9 +393,9 @@ - (void)testAnnouncesRouteChangesWhenNoNamesRoute { // Notification should focus first focusable node, which is node1. XCTAssertEqual([accessibility_notifications count], 1ul); - SemanticsObject* focusObject = accessibility_notifications[0][@"argument"]; - XCTAssertEqual([focusObject uid], 2); - XCTAssertEqualObjects([focusObject accessibilityLabel], @"node2"); + id focusObject = accessibility_notifications[0][@"argument"]; + XCTAssertTrue([focusObject isKindOfClass:[NSString class]]); + XCTAssertEqualObjects(focusObject, @"node1"); XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue], UIAccessibilityScreenChangedNotification); } diff --git a/shell/platform/darwin/ios/ios_external_texture_gl.h b/shell/platform/darwin/ios/ios_external_texture_gl.h index 09220a3d3564b..b7ad8b4daefd5 100644 --- a/shell/platform/darwin/ios/ios_external_texture_gl.h +++ b/shell/platform/darwin/ios/ios_external_texture_gl.h @@ -60,9 +60,9 @@ class IOSExternalTextureGL final : public Texture { void CreateRGBATextureFromPixelBuffer(); - sk_sp CreateImageFromYUVTextures(GrContext* context, const SkRect& bounds); + sk_sp CreateImageFromYUVTextures(GrDirectContext* context, const SkRect& bounds); - sk_sp CreateImageFromRGBATexture(GrContext* context, const SkRect& bounds); + sk_sp CreateImageFromRGBATexture(GrDirectContext* context, const SkRect& bounds); FML_DISALLOW_COPY_AND_ASSIGN(IOSExternalTextureGL); }; diff --git a/shell/platform/darwin/ios/ios_external_texture_gl.mm b/shell/platform/darwin/ios/ios_external_texture_gl.mm index 688e3072ed130..1429add5f283e 100644 --- a/shell/platform/darwin/ios/ios_external_texture_gl.mm +++ b/shell/platform/darwin/ios/ios_external_texture_gl.mm @@ -95,7 +95,7 @@ } } -sk_sp IOSExternalTextureGL::CreateImageFromRGBATexture(GrContext* context, +sk_sp IOSExternalTextureGL::CreateImageFromRGBATexture(GrDirectContext* context, const SkRect& bounds) { GrGLTextureInfo textureInfo = {CVOpenGLESTextureGetTarget(texture_ref_), CVOpenGLESTextureGetName(texture_ref_), GL_RGBA8_OES}; @@ -106,7 +106,7 @@ return image; } -sk_sp IOSExternalTextureGL::CreateImageFromYUVTextures(GrContext* context, +sk_sp IOSExternalTextureGL::CreateImageFromYUVTextures(GrDirectContext* context, const SkRect& bounds) { GrGLTextureInfo yTextureInfo = {CVOpenGLESTextureGetTarget(y_texture_ref_), CVOpenGLESTextureGetName(y_texture_ref_), GR_GL_LUMINANCE8}; diff --git a/shell/platform/darwin/ios/ios_external_view_embedder.h b/shell/platform/darwin/ios/ios_external_view_embedder.h new file mode 100644 index 0000000000000..e717c2ef2a167 --- /dev/null +++ b/shell/platform/darwin/ios/ios_external_view_embedder.h @@ -0,0 +1,71 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_PLATFORM_DARWIN_IOS_IOS_EXTERNAL_VIEW_EMBEDDER_H_ +#define FLUTTER_SHELL_PLATFORM_DARWIN_IOS_IOS_EXTERNAL_VIEW_EMBEDDER_H_ + +#include "flutter/flow/embedded_views.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h" + +namespace flutter { + +class IOSExternalViewEmbedder : public ExternalViewEmbedder { + public: + IOSExternalViewEmbedder( + FlutterPlatformViewsController* platform_views_controller, + std::shared_ptr context); + + // |ExternalViewEmbedder| + virtual ~IOSExternalViewEmbedder() override; + + private: + FlutterPlatformViewsController* platform_views_controller_; + std::shared_ptr ios_context_; + + // |ExternalViewEmbedder| + SkCanvas* GetRootCanvas() override; + + // |ExternalViewEmbedder| + void CancelFrame() override; + + // |ExternalViewEmbedder| + void BeginFrame( + SkISize frame_size, + GrDirectContext* context, + double device_pixel_ratio, + fml::RefPtr raster_thread_merger) override; + + // |ExternalViewEmbedder| + void PrerollCompositeEmbeddedView( + int view_id, + std::unique_ptr params) override; + + // |ExternalViewEmbedder| + PostPrerollResult PostPrerollAction( + fml::RefPtr raster_thread_merger) override; + + // |ExternalViewEmbedder| + std::vector GetCurrentCanvases() override; + + // |ExternalViewEmbedder| + SkCanvas* CompositeEmbeddedView(int view_id) override; + + // |ExternalViewEmbedder| + void SubmitFrame(GrDirectContext* context, + std::unique_ptr frame) override; + + // |ExternalViewEmbedder| + void EndFrame( + bool should_resubmit_frame, + fml::RefPtr raster_thread_merger) override; + + // |ExternalViewEmbedder| + bool SupportsDynamicThreadMerging() override; + + FML_DISALLOW_COPY_AND_ASSIGN(IOSExternalViewEmbedder); +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_IOS_EXTERNAL_VIEW_EMBEDDER_H_ diff --git a/shell/platform/darwin/ios/ios_external_view_embedder.mm b/shell/platform/darwin/ios/ios_external_view_embedder.mm new file mode 100644 index 0000000000000..8939b49116cbd --- /dev/null +++ b/shell/platform/darwin/ios/ios_external_view_embedder.mm @@ -0,0 +1,96 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "flutter/shell/platform/darwin/ios/ios_external_view_embedder.h" + +namespace flutter { + +IOSExternalViewEmbedder::IOSExternalViewEmbedder( + FlutterPlatformViewsController* platform_views_controller, + std::shared_ptr context) + : platform_views_controller_(platform_views_controller), ios_context_(context) { + FML_CHECK(ios_context_); +} + +IOSExternalViewEmbedder::~IOSExternalViewEmbedder() = default; + +// |ExternalViewEmbedder| +SkCanvas* IOSExternalViewEmbedder::GetRootCanvas() { + // On iOS, the root surface is created from the on-screen render target. Only the surfaces for the + // various overlays are controlled by this class. + return nullptr; +} + +// |ExternalViewEmbedder| +void IOSExternalViewEmbedder::CancelFrame() { + TRACE_EVENT0("flutter", "IOSExternalViewEmbedder::CancelFrame"); + FML_CHECK(platform_views_controller_); + platform_views_controller_->CancelFrame(); +} + +// |ExternalViewEmbedder| +void IOSExternalViewEmbedder::BeginFrame( + SkISize frame_size, + GrDirectContext* context, + double device_pixel_ratio, + fml::RefPtr raster_thread_merger) { + TRACE_EVENT0("flutter", "IOSExternalViewEmbedder::BeginFrame"); + FML_CHECK(platform_views_controller_); + platform_views_controller_->SetFrameSize(frame_size); +} + +// |ExternalViewEmbedder| +void IOSExternalViewEmbedder::PrerollCompositeEmbeddedView( + int view_id, + std::unique_ptr params) { + TRACE_EVENT0("flutter", "IOSExternalViewEmbedder::PrerollCompositeEmbeddedView"); + FML_CHECK(platform_views_controller_); + platform_views_controller_->PrerollCompositeEmbeddedView(view_id, std::move(params)); +} + +// |ExternalViewEmbedder| +PostPrerollResult IOSExternalViewEmbedder::PostPrerollAction( + fml::RefPtr raster_thread_merger) { + TRACE_EVENT0("flutter", "IOSExternalViewEmbedder::PostPrerollAction"); + FML_CHECK(platform_views_controller_); + PostPrerollResult result = platform_views_controller_->PostPrerollAction(raster_thread_merger); + return result; +} + +// |ExternalViewEmbedder| +std::vector IOSExternalViewEmbedder::GetCurrentCanvases() { + FML_CHECK(platform_views_controller_); + return platform_views_controller_->GetCurrentCanvases(); +} + +// |ExternalViewEmbedder| +SkCanvas* IOSExternalViewEmbedder::CompositeEmbeddedView(int view_id) { + TRACE_EVENT0("flutter", "IOSExternalViewEmbedder::CompositeEmbeddedView"); + FML_CHECK(platform_views_controller_); + return platform_views_controller_->CompositeEmbeddedView(view_id); +} + +// |ExternalViewEmbedder| +void IOSExternalViewEmbedder::SubmitFrame(GrDirectContext* context, + std::unique_ptr frame) { + TRACE_EVENT0("flutter", "IOSExternalViewEmbedder::SubmitFrame"); + FML_CHECK(platform_views_controller_); + platform_views_controller_->SubmitFrame(std::move(context), ios_context_, std::move(frame)); + TRACE_EVENT0("flutter", "IOSExternalViewEmbedder::DidSubmitFrame"); +} + +// |ExternalViewEmbedder| +void IOSExternalViewEmbedder::EndFrame(bool should_resubmit_frame, + fml::RefPtr raster_thread_merger) { + TRACE_EVENT0("flutter", "IOSExternalViewEmbedder::EndFrame"); + FML_CHECK(platform_views_controller_); + return platform_views_controller_->EndFrame(should_resubmit_frame, raster_thread_merger); +} + +// |ExternalViewEmbedder| +bool IOSExternalViewEmbedder::SupportsDynamicThreadMerging() { + return true; +} + +} // namespace flutter diff --git a/shell/platform/darwin/ios/ios_surface.h b/shell/platform/darwin/ios/ios_surface.h index a01f9f15caeed..8935b7537bdae 100644 --- a/shell/platform/darwin/ios/ios_surface.h +++ b/shell/platform/darwin/ios/ios_surface.h @@ -6,6 +6,7 @@ #define FLUTTER_SHELL_PLATFORM_DARWIN_IOS_IOS_SURFACE_H_ #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h" +#import "flutter/shell/platform/darwin/ios/ios_external_view_embedder.h" #include @@ -22,18 +23,19 @@ namespace flutter { // mechanism which is still in a release preview. bool IsIosEmbeddedViewsPreviewEnabled(); -class IOSSurface : public ExternalViewEmbedder { +class IOSSurface { public: static std::unique_ptr Create( std::shared_ptr context, fml::scoped_nsobject layer, FlutterPlatformViewsController* platform_views_controller); - // |ExternalViewEmbedder| - virtual ~IOSSurface(); - std::shared_ptr GetContext() const; + std::shared_ptr GetSurfaceExternalViewEmbedder() const; + + virtual ~IOSSurface(); + virtual bool IsValid() const = 0; virtual void UpdateStorageSizeIfNecessary() = 0; @@ -51,45 +53,8 @@ class IOSSurface : public ExternalViewEmbedder { private: std::shared_ptr ios_context_; - FlutterPlatformViewsController* platform_views_controller_; - - // |ExternalViewEmbedder| - SkCanvas* GetRootCanvas() override; - - // |ExternalViewEmbedder| - void CancelFrame() override; - - // |ExternalViewEmbedder| - void BeginFrame(SkISize frame_size, - GrDirectContext* context, - double device_pixel_ratio, - fml::RefPtr raster_thread_merger) override; + std::shared_ptr external_view_embedder_; - // |ExternalViewEmbedder| - void PrerollCompositeEmbeddedView(int view_id, - std::unique_ptr params) override; - - // |ExternalViewEmbedder| - PostPrerollResult PostPrerollAction( - fml::RefPtr raster_thread_merger) override; - - // |ExternalViewEmbedder| - std::vector GetCurrentCanvases() override; - - // |ExternalViewEmbedder| - SkCanvas* CompositeEmbeddedView(int view_id) override; - - // |ExternalViewEmbedder| - void SubmitFrame(GrDirectContext* context, std::unique_ptr frame) override; - - // |ExternalViewEmbedder| - void EndFrame(bool should_resubmit_frame, - fml::RefPtr raster_thread_merger) override; - - // |ExternalViewEmbedder| - bool SupportsDynamicThreadMerging() override; - - public: FML_DISALLOW_COPY_AND_ASSIGN(IOSSurface); }; diff --git a/shell/platform/darwin/ios/ios_surface.mm b/shell/platform/darwin/ios/ios_surface.mm index 976c71ee45a49..49b418a94390d 100644 --- a/shell/platform/darwin/ios/ios_surface.mm +++ b/shell/platform/darwin/ios/ios_surface.mm @@ -7,6 +7,8 @@ #import "flutter/shell/platform/darwin/ios/ios_surface_gl.h" #import "flutter/shell/platform/darwin/ios/ios_surface_software.h" +#include "flutter/shell/platform/darwin/ios/rendering_api_selection.h" + #if FLUTTER_SHELL_ENABLE_METAL #import "flutter/shell/platform/darwin/ios/ios_surface_metal.h" #endif // FLUTTER_SHELL_ENABLE_METAL @@ -30,13 +32,15 @@ } #if FLUTTER_SHELL_ENABLE_METAL - if ([layer.get() isKindOfClass:[CAMetalLayer class]]) { - return std::make_unique( - fml::scoped_nsobject( - reinterpret_cast([layer.get() retain])), // Metal layer - std::move(context), // context - platform_views_controller // platform views controller - ); + if (@available(iOS METAL_IOS_VERSION_BASELINE, *)) { + if ([layer.get() isKindOfClass:[CAMetalLayer class]]) { + return std::make_unique( + fml::scoped_nsobject( + reinterpret_cast([layer.get() retain])), // Metal layer + std::move(context), // context + platform_views_controller // platform views controller + ); + } } #endif // FLUTTER_SHELL_ENABLE_METAL @@ -49,8 +53,10 @@ IOSSurface::IOSSurface(std::shared_ptr ios_context, FlutterPlatformViewsController* platform_views_controller) - : ios_context_(std::move(ios_context)), platform_views_controller_(platform_views_controller) { + : ios_context_(std::move(ios_context)) { FML_DCHECK(ios_context_); + external_view_embedder_ = + std::make_shared(platform_views_controller, ios_context_); } IOSSurface::~IOSSurface() = default; @@ -59,80 +65,8 @@ return ios_context_; } -// |ExternalViewEmbedder| -SkCanvas* IOSSurface::GetRootCanvas() { - // On iOS, the root surface is created from the on-screen render target. Only the surfaces for the - // various overlays are controlled by this class. - return nullptr; -} - -// |ExternalViewEmbedder| -void IOSSurface::CancelFrame() { - TRACE_EVENT0("flutter", "IOSSurface::CancelFrame"); - FML_CHECK(platform_views_controller_ != nullptr); - platform_views_controller_->CancelFrame(); -} - -// |ExternalViewEmbedder| -void IOSSurface::BeginFrame(SkISize frame_size, - GrDirectContext* context, - double device_pixel_ratio, - fml::RefPtr raster_thread_merger) { - TRACE_EVENT0("flutter", "IOSSurface::BeginFrame"); - FML_CHECK(platform_views_controller_ != nullptr); - platform_views_controller_->SetFrameSize(frame_size); -} - -// |ExternalViewEmbedder| -void IOSSurface::PrerollCompositeEmbeddedView(int view_id, - std::unique_ptr params) { - TRACE_EVENT0("flutter", "IOSSurface::PrerollCompositeEmbeddedView"); - - FML_CHECK(platform_views_controller_ != nullptr); - platform_views_controller_->PrerollCompositeEmbeddedView(view_id, std::move(params)); -} - -// |ExternalViewEmbedder| -PostPrerollResult IOSSurface::PostPrerollAction( - fml::RefPtr raster_thread_merger) { - TRACE_EVENT0("flutter", "IOSSurface::PostPrerollAction"); - FML_CHECK(platform_views_controller_ != nullptr); - PostPrerollResult result = platform_views_controller_->PostPrerollAction(raster_thread_merger); - return result; -} - -// |ExternalViewEmbedder| -std::vector IOSSurface::GetCurrentCanvases() { - FML_CHECK(platform_views_controller_ != nullptr); - return platform_views_controller_->GetCurrentCanvases(); -} - -// |ExternalViewEmbedder| -SkCanvas* IOSSurface::CompositeEmbeddedView(int view_id) { - TRACE_EVENT0("flutter", "IOSSurface::CompositeEmbeddedView"); - FML_CHECK(platform_views_controller_ != nullptr); - return platform_views_controller_->CompositeEmbeddedView(view_id); -} - -// |ExternalViewEmbedder| -void IOSSurface::SubmitFrame(GrDirectContext* context, std::unique_ptr frame) { - TRACE_EVENT0("flutter", "IOSSurface::SubmitFrame"); - FML_CHECK(platform_views_controller_ != nullptr); - platform_views_controller_->SubmitFrame(std::move(context), ios_context_, std::move(frame)); - TRACE_EVENT0("flutter", "IOSSurface::DidSubmitFrame"); -} - -// |ExternalViewEmbedder| -void IOSSurface::EndFrame(bool should_resubmit_frame, - fml::RefPtr raster_thread_merger) { - TRACE_EVENT0("flutter", "IOSSurface::EndFrame"); - FML_CHECK(platform_views_controller_ != nullptr); - return platform_views_controller_->EndFrame(should_resubmit_frame, raster_thread_merger); -} - -// |ExternalViewEmbedder| -bool IOSSurface::SupportsDynamicThreadMerging() { - return true; +std::shared_ptr IOSSurface::GetSurfaceExternalViewEmbedder() const { + return external_view_embedder_; } } // namespace flutter diff --git a/shell/platform/darwin/ios/ios_surface_gl.mm b/shell/platform/darwin/ios/ios_surface_gl.mm index 3565365f73e7a..e114d5efee4a8 100644 --- a/shell/platform/darwin/ios/ios_surface_gl.mm +++ b/shell/platform/darwin/ios/ios_surface_gl.mm @@ -84,7 +84,7 @@ // |GPUSurfaceGLDelegate| ExternalViewEmbedder* IOSSurfaceGL::GetExternalViewEmbedder() { - return this; + return GetSurfaceExternalViewEmbedder().get(); } } // namespace flutter diff --git a/shell/platform/darwin/ios/ios_surface_metal.h b/shell/platform/darwin/ios/ios_surface_metal.h index 7d1e4d5d159a8..c449248eda633 100644 --- a/shell/platform/darwin/ios/ios_surface_metal.h +++ b/shell/platform/darwin/ios/ios_surface_metal.h @@ -8,12 +8,14 @@ #include "flutter/fml/macros.h" #include "flutter/shell/gpu/gpu_surface_delegate.h" #import "flutter/shell/platform/darwin/ios/ios_surface.h" +#include "third_party/skia/include/gpu/mtl/GrMtlTypes.h" @class CAMetalLayer; namespace flutter { -class IOSSurfaceMetal final : public IOSSurface, public GPUSurfaceDelegate { +class SK_API_AVAILABLE_CA_METAL_LAYER IOSSurfaceMetal final : public IOSSurface, + public GPUSurfaceDelegate { public: IOSSurfaceMetal(fml::scoped_nsobject layer, std::shared_ptr context, diff --git a/shell/platform/darwin/ios/ios_surface_metal.mm b/shell/platform/darwin/ios/ios_surface_metal.mm index e7e4d12fcbfc8..ee240bd96a29b 100644 --- a/shell/platform/darwin/ios/ios_surface_metal.mm +++ b/shell/platform/darwin/ios/ios_surface_metal.mm @@ -55,7 +55,7 @@ // |GPUSurfaceDelegate| ExternalViewEmbedder* IOSSurfaceMetal::GetExternalViewEmbedder() { - return this; + return GetSurfaceExternalViewEmbedder().get(); } } // namespace flutter diff --git a/shell/platform/darwin/ios/ios_surface_software.mm b/shell/platform/darwin/ios/ios_surface_software.mm index 1363bfea99fc2..7b5c2ee8ad723 100644 --- a/shell/platform/darwin/ios/ios_surface_software.mm +++ b/shell/platform/darwin/ios/ios_surface_software.mm @@ -124,7 +124,7 @@ // |GPUSurfaceSoftwareDelegate| ExternalViewEmbedder* IOSSurfaceSoftware::GetExternalViewEmbedder() { - return this; + return GetSurfaceExternalViewEmbedder().get(); } } // namespace flutter diff --git a/shell/platform/darwin/ios/rendering_api_selection.h b/shell/platform/darwin/ios/rendering_api_selection.h index f30a49a091e21..07fe13ccb198d 100644 --- a/shell/platform/darwin/ios/rendering_api_selection.h +++ b/shell/platform/darwin/ios/rendering_api_selection.h @@ -17,11 +17,23 @@ enum class IOSRenderingAPI { kMetal, }; -IOSRenderingAPI GetRenderingAPIForProcess(); +// Pass force_software to force software rendering. This is only respected on +// simulators. +IOSRenderingAPI GetRenderingAPIForProcess(bool force_software); -Class GetCoreAnimationLayerClassForRenderingAPI( - IOSRenderingAPI rendering_api = GetRenderingAPIForProcess()); +Class GetCoreAnimationLayerClassForRenderingAPI(IOSRenderingAPI rendering_api); } // namespace flutter +// Flutter supports Metal on all devices with Apple A7 SoC or above that have +// been updated to or past iOS 10.0. The processor was selected as it is the +// first version at which Metal was supported. The iOS version floor was +// selected due to the availability of features used by Skia. +// Support for Metal on simulators was added by Apple in the SDK for iOS 13. +#if TARGET_OS_SIMULATOR +#define METAL_IOS_VERSION_BASELINE 13.0 +#else +#define METAL_IOS_VERSION_BASELINE 10.0 +#endif // TARGET_OS_SIMULATOR + #endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_RENDERING_API_SELECTION_H_ diff --git a/shell/platform/darwin/ios/rendering_api_selection.mm b/shell/platform/darwin/ios/rendering_api_selection.mm index 6ee84bcc69edd..2cf23bcccd6d5 100644 --- a/shell/platform/darwin/ios/rendering_api_selection.mm +++ b/shell/platform/darwin/ios/rendering_api_selection.mm @@ -10,6 +10,7 @@ #if FLUTTER_SHELL_ENABLE_METAL #include #endif // FLUTTER_SHELL_ENABLE_METAL +#import #include "flutter/fml/logging.h" @@ -17,11 +18,8 @@ #if FLUTTER_SHELL_ENABLE_METAL bool ShouldUseMetalRenderer() { - // Flutter supports Metal on all devices with Apple A7 SoC or above that have been updated to or - // past iOS 10.0. The processor was selected as it is the first version at which Metal was - // supported. The iOS version floor was selected due to the availability of features used by Skia. bool ios_version_supports_metal = false; - if (@available(iOS 10.0, *)) { + if (@available(iOS METAL_IOS_VERSION_BASELINE, *)) { auto device = MTLCreateSystemDefaultDevice(); ios_version_supports_metal = [device supportsFeatureSet:MTLFeatureSet_iOS_GPUFamily1_v3]; } @@ -29,10 +27,17 @@ bool ShouldUseMetalRenderer() { } #endif // FLUTTER_SHELL_ENABLE_METAL -IOSRenderingAPI GetRenderingAPIForProcess() { -#if TARGET_IPHONE_SIMULATOR - return IOSRenderingAPI::kSoftware; -#endif // TARGET_IPHONE_SIMULATOR +IOSRenderingAPI GetRenderingAPIForProcess(bool force_software) { +#if TARGET_OS_SIMULATOR + if (force_software) { + return IOSRenderingAPI::kSoftware; + } +#else + if (force_software) { + FML_LOG(WARNING) << "The --enable-software-rendering is only supported on Simulator targets " + "and will be ignored."; + } +#endif // TARGET_OS_SIMULATOR #if FLUTTER_SHELL_ENABLE_METAL static bool should_use_metal = ShouldUseMetalRenderer(); @@ -40,7 +45,14 @@ IOSRenderingAPI GetRenderingAPIForProcess() { return IOSRenderingAPI::kMetal; } #endif // FLUTTER_SHELL_ENABLE_METAL + + // OpenGL will be emulated using software rendering by Apple on the simulator, so we use the + // Skia software rendering since it performs a little better than the emulated OpenGL. +#if TARGET_OS_SIMULATOR + return IOSRenderingAPI::kSoftware; +#else return IOSRenderingAPI::kOpenGLES; +#endif // TARGET_OS_SIMULATOR } Class GetCoreAnimationLayerClassForRenderingAPI(IOSRenderingAPI rendering_api) { @@ -49,10 +61,12 @@ Class GetCoreAnimationLayerClassForRenderingAPI(IOSRenderingAPI rendering_api) { return [CALayer class]; case IOSRenderingAPI::kOpenGLES: return [CAEAGLLayer class]; -#if !TARGET_IPHONE_SIMULATOR case IOSRenderingAPI::kMetal: - return [CAMetalLayer class]; -#endif // !TARGET_IPHONE_SIMULATOR + if (@available(iOS METAL_IOS_VERSION_BASELINE, *)) { + return [CAMetalLayer class]; + } + FML_CHECK(false) << "Metal availability should already have been checked"; + break; default: break; } diff --git a/shell/platform/darwin/macos/BUILD.gn b/shell/platform/darwin/macos/BUILD.gn index 58a9e66bcb180..9800139a8a1cf 100644 --- a/shell/platform/darwin/macos/BUILD.gn +++ b/shell/platform/darwin/macos/BUILD.gn @@ -109,6 +109,8 @@ executable("flutter_desktop_darwin_unittests") { cflags_objcc = [ "-fobjc-arc" ] + ldflags = [ "-ObjC" ] + deps = [ ":flutter_desktop_darwin_fixtures", ":flutter_framework_source", diff --git a/shell/platform/darwin/macos/framework/Headers/FlutterDartProject.h b/shell/platform/darwin/macos/framework/Headers/FlutterDartProject.h index ab4f0d9e60bab..e87ddf54eaded 100644 --- a/shell/platform/darwin/macos/framework/Headers/FlutterDartProject.h +++ b/shell/platform/darwin/macos/framework/Headers/FlutterDartProject.h @@ -36,6 +36,16 @@ FLUTTER_EXPORT */ @property(nonatomic) bool enableMirrors; +/** + * An NSArray of NSStrings to be passed as command line arguments to the Dart entrypoint. + * + * If this is not explicitly set, this will default to the contents of + * [NSProcessInfo arguments], without the binary name. + * + * Set this to nil to pass no arguments to the Dart entrypoint. + */ +@property(nonatomic, nullable) NSArray* dartEntrypointArguments; + @end #endif // FLUTTER_FLUTTERDARTPROJECT_H_ diff --git a/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate.mm b/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate.mm index b251a459beac0..0442fdd70232d 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate.mm @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h" diff --git a/shell/platform/darwin/macos/framework/Source/FlutterDartProject.mm b/shell/platform/darwin/macos/framework/Source/FlutterDartProject.mm index a1648b332a156..6343729d09ac7 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterDartProject.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterDartProject.mm @@ -27,6 +27,10 @@ - (instancetype)initWithPrecompiledDartBundle:(NSBundle*)bundle { NSAssert(self, @"Super init cannot be nil"); _dartBundle = bundle ?: [NSBundle bundleWithIdentifier:kAppBundleIdentifier]; + _dartEntrypointArguments = [[NSProcessInfo processInfo] arguments]; + // Remove the first element as it's the binary name + _dartEntrypointArguments = [_dartEntrypointArguments + subarrayWithRange:NSMakeRange(1, _dartEntrypointArguments.count - 1)]; return self; } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm index 359f13bd834b6..60968ceec0031 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h" @@ -28,6 +27,16 @@ static FlutterLocale FlutterLocaleFromNSLocale(NSLocale* locale) { return flutterLocale; } +namespace { + +struct AotDataDeleter { + void operator()(FlutterEngineAOTData aot_data) { FlutterEngineCollectAOTData(aot_data); } +}; + +using UniqueAotDataPtr = std::unique_ptr<_FlutterEngineAOTData, AotDataDeleter>; + +} + /** * Private interface declaration for FlutterEngine. */ @@ -75,6 +84,12 @@ - (BOOL)populateTextureWithIdentifier:(int64_t)textureID */ - (void)postMainThreadTask:(FlutterTask)task targetTimeInNanoseconds:(uint64_t)targetTime; +/** + * Loads the AOT snapshots and instructions from the elf bundle (app_elf_snapshot.so) if it is + * present in the assets directory. + */ +- (UniqueAotDataPtr)loadAOTData:(NSString*)assetsDir; + @end #pragma mark - @@ -184,6 +199,9 @@ @implementation FlutterEngine { // A mapping of textureID to internal FlutterExternalTextureGL adapter. NSMutableDictionary* _textures; + + // Pointer to the Dart AOT snapshot and instruction data. + UniqueAotDataPtr _aotData; } - (instancetype)initWithName:(NSString*)labelPrefix project:(FlutterDartProject*)project { @@ -245,6 +263,11 @@ - (BOOL)runWithEntrypoint:(NSString*)entrypoint { std::transform(switches.begin(), switches.end(), std::back_inserter(argv), [](const std::string& arg) -> const char* { return arg.c_str(); }); + std::vector dartEntrypointArgs; + for (NSString* argument in [_project dartEntrypointArguments]) { + dartEntrypointArgs.push_back([argument UTF8String]); + } + FlutterProjectArgs flutterArguments = {}; flutterArguments.struct_size = sizeof(FlutterProjectArgs); flutterArguments.assets_path = _project.assetsPath.UTF8String; @@ -253,6 +276,10 @@ - (BOOL)runWithEntrypoint:(NSString*)entrypoint { flutterArguments.command_line_argv = argv.size() > 0 ? argv.data() : nullptr; flutterArguments.platform_message_callback = (FlutterPlatformMessageCallback)OnPlatformMessage; flutterArguments.custom_dart_entrypoint = entrypoint.UTF8String; + flutterArguments.shutdown_dart_vm_when_done = true; + flutterArguments.dart_entrypoint_argc = dartEntrypointArgs.size(); + flutterArguments.dart_entrypoint_argv = dartEntrypointArgs.data(); + static size_t sTaskRunnerIdentifiers = 0; const FlutterTaskRunnerDescription cocoa_task_runner_description = { .struct_size = sizeof(FlutterTaskRunnerDescription), @@ -274,6 +301,11 @@ - (BOOL)runWithEntrypoint:(NSString*)entrypoint { }; flutterArguments.custom_task_runners = &custom_task_runners; + _aotData = [self loadAOTData:_project.assetsPath]; + if (_aotData) { + flutterArguments.aot_data = _aotData.get(); + } + FlutterEngineResult result = FlutterEngineInitialize( FLUTTER_ENGINE_VERSION, &rendererConfig, &flutterArguments, (__bridge void*)(self), &_engine); if (result != kSuccess) { @@ -293,6 +325,36 @@ - (BOOL)runWithEntrypoint:(NSString*)entrypoint { return YES; } +- (UniqueAotDataPtr)loadAOTData:(NSString*)assetsDir { + if (!FlutterEngineRunsAOTCompiledDartCode()) { + return nullptr; + } + + BOOL isDirOut = false; // required for NSFileManager fileExistsAtPath. + NSFileManager* fileManager = [NSFileManager defaultManager]; + + // This is the location where the test fixture places the snapshot file. + // For applications built by Flutter tool, this is in "App.framework". + NSString* elfPath = [NSString pathWithComponents:@[ assetsDir, @"app_elf_snapshot.so" ]]; + + if (![fileManager fileExistsAtPath:elfPath isDirectory:&isDirOut]) { + return nullptr; + } + + FlutterEngineAOTDataSource source = {}; + source.type = kFlutterEngineAOTDataSourceTypeElfPath; + source.elf_path = [elfPath cStringUsingEncoding:NSUTF8StringEncoding]; + + FlutterEngineAOTData data = nullptr; + auto result = FlutterEngineCreateAOTData(&source, &data); + if (result != kSuccess) { + NSLog(@"Failed to load AOT data from: %@", elfPath); + return nullptr; + } + + return UniqueAotDataPtr(data); +} + - (void)setViewController:(FlutterViewController*)controller { _viewController = controller; _mainOpenGLContext = controller.flutterView.openGLContext; diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngineUnittests.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngineUnittests.mm index 2637db3a85b0c..1ad6c564e7daf 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngineUnittests.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngineUnittests.mm @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h" diff --git a/shell/platform/darwin/macos/framework/Source/FlutterExternalTextureGL.mm b/shell/platform/darwin/macos/framework/Source/FlutterExternalTextureGL.mm index 45d1f624429fa..0314280e9f12d 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterExternalTextureGL.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterExternalTextureGL.mm @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterExternalTextureGL.h" diff --git a/shell/platform/darwin/macos/framework/Source/FlutterMouseCursorPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterMouseCursorPlugin.mm index d808d4cc644d4..e73331db17140 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterMouseCursorPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterMouseCursorPlugin.mm @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #import diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputModel.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputModel.mm index 4f576d251f0f7..5208925ec7340 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputModel.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputModel.mm @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputModel.h" diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index 08347d7af3ee5..46af8dd3953b1 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h" diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm index e30dbc7d9ae3f..cc5eaba49bf39 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h" diff --git a/shell/platform/embedder/embedder.cc b/shell/platform/embedder/embedder.cc index 506a5418cff27..c72a272e44990 100644 --- a/shell/platform/embedder/embedder.cc +++ b/shell/platform/embedder/embedder.cc @@ -1,8 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT -// FLUTTER_NOLINT #define FML_USED_ON_EMBEDDER #define RAPIDJSON_HAS_STDSTRING 1 @@ -651,13 +649,13 @@ FlutterEngineResult FlutterEngineCreateAOTData( } FlutterEngineResult FlutterEngineCollectAOTData(FlutterEngineAOTData data) { - if (data) { - data->loaded_elf = nullptr; - data->vm_snapshot_data = nullptr; - data->vm_snapshot_instrs = nullptr; - data->vm_isolate_data = nullptr; - data->vm_isolate_instrs = nullptr; + if (!data) { + // Deleting a null object should be a no-op. + return kSuccess; } + + // Created in a unique pointer in `FlutterEngineCreateAOTData`. + delete data; return kSuccess; } @@ -843,9 +841,8 @@ FlutterEngineResult FlutterEngineInitialize(size_t version, if (SAFE_ACCESS(args, root_isolate_create_callback, nullptr) != nullptr) { VoidCallback callback = SAFE_ACCESS(args, root_isolate_create_callback, nullptr); - settings.root_isolate_create_callback = [callback, user_data]() { - callback(user_data); - }; + settings.root_isolate_create_callback = + [callback, user_data](const auto& isolate) { callback(user_data); }; } flutter::PlatformViewEmbedder::UpdateSemanticsNodesCallback @@ -1126,6 +1123,20 @@ FlutterEngineResult FlutterEngineInitialize(size_t version, } } + if (SAFE_ACCESS(args, dart_entrypoint_argc, 0) > 0) { + if (SAFE_ACCESS(args, dart_entrypoint_argv, nullptr) == nullptr) { + return LOG_EMBEDDER_ERROR(kInvalidArguments, + "Could not determine Dart entrypoint arguments " + "as dart_entrypoint_argc " + "was set, but dart_entrypoint_argv was null."); + } + std::vector arguments(args->dart_entrypoint_argc); + for (int i = 0; i < args->dart_entrypoint_argc; ++i) { + arguments[i] = std::string{args->dart_entrypoint_argv[i]}; + } + settings.dart_entrypoint_args = std::move(arguments); + } + if (!run_configuration.IsValid()) { return LOG_EMBEDDER_ERROR( kInvalidArguments, diff --git a/shell/platform/embedder/embedder.h b/shell/platform/embedder/embedder.h index 8ed2dadfac37f..51d832bfe5397 100644 --- a/shell/platform/embedder/embedder.h +++ b/shell/platform/embedder/embedder.h @@ -1368,6 +1368,19 @@ typedef struct { /// matches what the platform would natively resolve to as possible. FlutterComputePlatformResolvedLocaleCallback compute_platform_resolved_locale_callback; + + /// The command line argument count for arguments passed through to the Dart + /// entrypoint. + int dart_entrypoint_argc; + + /// The command line arguments passed through to the Dart entrypoint. The + /// strings must be `NULL` terminated. + /// + /// The strings will be copied out and so any strings passed in here can + /// be safely collected after initializing the engine with + /// `FlutterProjectArgs`. + const char* const* dart_entrypoint_argv; + } FlutterProjectArgs; //------------------------------------------------------------------------------ diff --git a/shell/platform/embedder/embedder_surface_gl.cc b/shell/platform/embedder/embedder_surface_gl.cc index 6b88fcb9d14e1..05ff3e95ed3cb 100644 --- a/shell/platform/embedder/embedder_surface_gl.cc +++ b/shell/platform/embedder/embedder_surface_gl.cc @@ -11,10 +11,10 @@ namespace flutter { EmbedderSurfaceGL::EmbedderSurfaceGL( GLDispatchTable gl_dispatch_table, bool fbo_reset_after_present, - std::unique_ptr external_view_embedder) + std::shared_ptr external_view_embedder) : gl_dispatch_table_(gl_dispatch_table), fbo_reset_after_present_(fbo_reset_after_present), - external_view_embedder_(std::move(external_view_embedder)) { + external_view_embedder_(external_view_embedder) { // Make sure all required members of the dispatch table are checked. if (!gl_dispatch_table_.gl_make_current_callback || !gl_dispatch_table_.gl_clear_current_callback || diff --git a/shell/platform/embedder/embedder_surface_gl.h b/shell/platform/embedder/embedder_surface_gl.h index 42141d767d074..43ad1e4778bf6 100644 --- a/shell/platform/embedder/embedder_surface_gl.h +++ b/shell/platform/embedder/embedder_surface_gl.h @@ -29,7 +29,7 @@ class EmbedderSurfaceGL final : public EmbedderSurface, EmbedderSurfaceGL( GLDispatchTable gl_dispatch_table, bool fbo_reset_after_present, - std::unique_ptr external_view_embedder); + std::shared_ptr external_view_embedder); ~EmbedderSurfaceGL() override; @@ -38,7 +38,7 @@ class EmbedderSurfaceGL final : public EmbedderSurface, GLDispatchTable gl_dispatch_table_; bool fbo_reset_after_present_; - std::unique_ptr external_view_embedder_; + std::shared_ptr external_view_embedder_; // |EmbedderSurface| bool IsValid() const override; diff --git a/shell/platform/embedder/embedder_surface_software.cc b/shell/platform/embedder/embedder_surface_software.cc index 9472404f0d429..914087ec1081b 100644 --- a/shell/platform/embedder/embedder_surface_software.cc +++ b/shell/platform/embedder/embedder_surface_software.cc @@ -11,9 +11,9 @@ namespace flutter { EmbedderSurfaceSoftware::EmbedderSurfaceSoftware( SoftwareDispatchTable software_dispatch_table, - std::unique_ptr external_view_embedder) + std::shared_ptr external_view_embedder) : software_dispatch_table_(software_dispatch_table), - external_view_embedder_(std::move(external_view_embedder)) { + external_view_embedder_(external_view_embedder) { if (!software_dispatch_table_.software_present_backing_store) { return; } diff --git a/shell/platform/embedder/embedder_surface_software.h b/shell/platform/embedder/embedder_surface_software.h index 8ed55ceec5b6a..a08122809f92b 100644 --- a/shell/platform/embedder/embedder_surface_software.h +++ b/shell/platform/embedder/embedder_surface_software.h @@ -22,7 +22,7 @@ class EmbedderSurfaceSoftware final : public EmbedderSurface, EmbedderSurfaceSoftware( SoftwareDispatchTable software_dispatch_table, - std::unique_ptr external_view_embedder); + std::shared_ptr external_view_embedder); ~EmbedderSurfaceSoftware() override; @@ -30,7 +30,7 @@ class EmbedderSurfaceSoftware final : public EmbedderSurface, bool valid_ = false; SoftwareDispatchTable software_dispatch_table_; sk_sp sk_surface_; - std::unique_ptr external_view_embedder_; + std::shared_ptr external_view_embedder_; // |EmbedderSurface| bool IsValid() const override; diff --git a/shell/platform/embedder/fixtures/main.dart b/shell/platform/embedder/fixtures/main.dart index de4cf422b662f..1df95c1c8727c 100644 --- a/shell/platform/embedder/fixtures/main.dart +++ b/shell/platform/embedder/fixtures/main.dart @@ -79,12 +79,12 @@ class SemanticsActionData { const SemanticsActionData(this.id, this.action, this.args); final int id; final SemanticsAction action; - final ByteData args; + final ByteData? args; } Future get semanticsAction { final Completer actionReceived = Completer(); - window.onSemanticsAction = (int id, SemanticsAction action, ByteData args) { + window.onSemanticsAction = (int id, SemanticsAction action, ByteData? args) { actionReceived.complete(SemanticsActionData(id, action, args)); }; return actionReceived.future; @@ -115,12 +115,52 @@ void a11y_main() async { // ignore: non_constant_identifier_names transform: kTestTransform, childrenInTraversalOrder: Int32List.fromList([84, 96]), childrenInHitTestOrder: Int32List.fromList([96, 84]), + actions: 0, + flags: 0, + maxValueLength: 0, + currentValueLength: 0, + textSelectionBase: 0, + textSelectionExtent: 0, + platformViewId: 0, + scrollChildren: 0, + scrollIndex: 0, + scrollPosition: 0.0, + scrollExtentMax: 0.0, + scrollExtentMin: 0.0, + elevation: 0.0, + thickness: 0.0, + hint: "", + value: "", + increasedValue: "", + decreasedValue: "", + additionalActions: Int32List(0), ) ..updateNode( id: 84, label: 'B: leaf', rect: Rect.fromLTRB(40.0, 40.0, 80.0, 80.0), transform: kTestTransform, + actions: 0, + flags: 0, + maxValueLength: 0, + currentValueLength: 0, + textSelectionBase: 0, + textSelectionExtent: 0, + platformViewId: 0, + scrollChildren: 0, + scrollIndex: 0, + scrollPosition: 0.0, + scrollExtentMax: 0.0, + scrollExtentMin: 0.0, + elevation: 0.0, + thickness: 0.0, + hint: "", + value: "", + increasedValue: "", + decreasedValue: "", + additionalActions: Int32List(0), + childrenInHitTestOrder: Int32List(0), + childrenInTraversalOrder: Int32List(0), ) ..updateNode( id: 96, @@ -129,6 +169,25 @@ void a11y_main() async { // ignore: non_constant_identifier_names transform: kTestTransform, childrenInTraversalOrder: Int32List.fromList([128]), childrenInHitTestOrder: Int32List.fromList([128]), + actions: 0, + flags: 0, + maxValueLength: 0, + currentValueLength: 0, + textSelectionBase: 0, + textSelectionExtent: 0, + platformViewId: 0, + scrollChildren: 0, + scrollIndex: 0, + scrollPosition: 0.0, + scrollExtentMax: 0.0, + scrollExtentMin: 0.0, + elevation: 0.0, + thickness: 0.0, + hint: "", + value: "", + increasedValue: "", + decreasedValue: "", + additionalActions: Int32List(0), ) ..updateNode( id: 128, @@ -137,6 +196,25 @@ void a11y_main() async { // ignore: non_constant_identifier_names transform: kTestTransform, additionalActions: Int32List.fromList([21]), platformViewId: 0x3f3, + actions: 0, + flags: 0, + maxValueLength: 0, + currentValueLength: 0, + textSelectionBase: 0, + textSelectionExtent: 0, + scrollChildren: 0, + scrollIndex: 0, + scrollPosition: 0.0, + scrollExtentMax: 0.0, + scrollExtentMin: 0.0, + elevation: 0.0, + thickness: 0.0, + hint: "", + value: "", + increasedValue: "", + decreasedValue: "", + childrenInHitTestOrder: Int32List(0), + childrenInTraversalOrder: Int32List(0), ) ..updateCustomAction( id: 21, @@ -148,7 +226,7 @@ void a11y_main() async { // ignore: non_constant_identifier_names // Await semantics action from embedder. final SemanticsActionData data = await semanticsAction; - final List actionArgs = [data.args.getInt8(0), data.args.getInt8(1)]; + final List actionArgs = [data.args!.getInt8(0), data.args!.getInt8(1)]; notifySemanticsAction(data.id, data.action.index, actionArgs); // Await semantics disabled from embedder. @@ -159,20 +237,20 @@ void a11y_main() async { // ignore: non_constant_identifier_names @pragma('vm:entry-point') void platform_messages_response() { - window.onPlatformMessage = (String name, ByteData data, PlatformMessageResponseCallback callback) { - callback(data); + window.onPlatformMessage = (String name, ByteData? data, PlatformMessageResponseCallback? callback) { + callback!(data); }; signalNativeTest(); } @pragma('vm:entry-point') void platform_messages_no_response() { - window.onPlatformMessage = (String name, ByteData data, PlatformMessageResponseCallback callback) { - var list = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); + window.onPlatformMessage = (String name, ByteData? data, PlatformMessageResponseCallback? callback) { + var list = data!.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); signalNativeMessage(utf8.decode(list)); // This does nothing because no one is listening on the other side. But complete the loop anyway // to make sure all null checking on response handles in the engine is in place. - callback(data); + callback!(data); }; signalNativeTest(); } @@ -180,10 +258,10 @@ void platform_messages_no_response() { @pragma('vm:entry-point') void null_platform_messages() { window.onPlatformMessage = - (String name, ByteData data, PlatformMessageResponseCallback callback) { + (String name, ByteData? data, PlatformMessageResponseCallback? callback) { // This checks if the platform_message null data is converted to Flutter null. signalNativeMessage((null == data).toString()); - callback(data); + callback!(data); }; signalNativeTest(); } @@ -507,7 +585,7 @@ void can_display_platform_view_with_pixel_ratio() { @pragma('vm:entry-point') void can_receive_locale_updates() { window.onLocaleChanged = (){ - signalNativeCount(window.locales.length); + signalNativeCount(window.locales!.length); }; signalNativeTest(); } @@ -733,3 +811,10 @@ void render_targets_are_recycled() { }; window.scheduleFrame(); } + +void nativeArgumentsCallback(List args) native 'NativeArgumentsCallback'; + +@pragma('vm:entry-point') +void dart_entrypoint_args(List args) { + nativeArgumentsCallback(args); +} diff --git a/shell/platform/embedder/platform_view_embedder.cc b/shell/platform/embedder/platform_view_embedder.cc index 8d06b3ce164a6..de51eb31afcc4 100644 --- a/shell/platform/embedder/platform_view_embedder.cc +++ b/shell/platform/embedder/platform_view_embedder.cc @@ -11,11 +11,12 @@ PlatformViewEmbedder::PlatformViewEmbedder( flutter::TaskRunners task_runners, EmbedderSurfaceSoftware::SoftwareDispatchTable software_dispatch_table, PlatformDispatchTable platform_dispatch_table, - std::unique_ptr external_view_embedder) + std::shared_ptr external_view_embedder) : PlatformView(delegate, std::move(task_runners)), - embedder_surface_(std::make_unique( - software_dispatch_table, - std::move(external_view_embedder))), + external_view_embedder_(external_view_embedder), + embedder_surface_( + std::make_unique(software_dispatch_table, + external_view_embedder_)), platform_dispatch_table_(platform_dispatch_table) {} #ifdef SHELL_ENABLE_GL @@ -25,12 +26,13 @@ PlatformViewEmbedder::PlatformViewEmbedder( EmbedderSurfaceGL::GLDispatchTable gl_dispatch_table, bool fbo_reset_after_present, PlatformDispatchTable platform_dispatch_table, - std::unique_ptr external_view_embedder) + std::shared_ptr external_view_embedder) : PlatformView(delegate, std::move(task_runners)), - embedder_surface_(std::make_unique( - gl_dispatch_table, - fbo_reset_after_present, - std::move(external_view_embedder))), + external_view_embedder_(external_view_embedder), + embedder_surface_( + std::make_unique(gl_dispatch_table, + fbo_reset_after_present, + external_view_embedder_)), platform_dispatch_table_(platform_dispatch_table) {} #endif diff --git a/shell/platform/embedder/platform_view_embedder.h b/shell/platform/embedder/platform_view_embedder.h index 85d3a91290dee..6fefcf8b20a9d 100644 --- a/shell/platform/embedder/platform_view_embedder.h +++ b/shell/platform/embedder/platform_view_embedder.h @@ -7,6 +7,7 @@ #include +#include "flow/embedded_views.h" #include "flutter/fml/macros.h" #include "flutter/shell/common/platform_view.h" #include "flutter/shell/platform/embedder/embedder.h" @@ -49,7 +50,7 @@ class PlatformViewEmbedder final : public PlatformView { flutter::TaskRunners task_runners, EmbedderSurfaceSoftware::SoftwareDispatchTable software_dispatch_table, PlatformDispatchTable platform_dispatch_table, - std::unique_ptr external_view_embedder); + std::shared_ptr external_view_embedder); #ifdef SHELL_ENABLE_GL // Creates a platform view that sets up an OpenGL rasterizer. @@ -59,7 +60,7 @@ class PlatformViewEmbedder final : public PlatformView { EmbedderSurfaceGL::GLDispatchTable gl_dispatch_table, bool fbo_reset_after_present, PlatformDispatchTable platform_dispatch_table, - std::unique_ptr external_view_embedder); + std::shared_ptr external_view_embedder); #endif ~PlatformViewEmbedder() override; @@ -74,6 +75,7 @@ class PlatformViewEmbedder final : public PlatformView { fml::RefPtr message) override; private: + std::shared_ptr external_view_embedder_; std::unique_ptr embedder_surface_; PlatformDispatchTable platform_dispatch_table_; diff --git a/shell/platform/embedder/tests/embedder_a11y_unittests.cc b/shell/platform/embedder/tests/embedder_a11y_unittests.cc index 568505b9ff041..b1d27f8a071a1 100644 --- a/shell/platform/embedder/tests/embedder_a11y_unittests.cc +++ b/shell/platform/embedder/tests/embedder_a11y_unittests.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT // Allow access to fml::MessageLoop::GetCurrent() in order to flush platform // thread tasks. diff --git a/shell/platform/embedder/tests/embedder_config_builder.cc b/shell/platform/embedder/tests/embedder_config_builder.cc index 5c1f741996257..93d4c6af916f2 100644 --- a/shell/platform/embedder/tests/embedder_config_builder.cc +++ b/shell/platform/embedder/tests/embedder_config_builder.cc @@ -217,6 +217,14 @@ void EmbedderConfigBuilder::AddCommandLineArgument(std::string arg) { command_line_arguments_.emplace_back(std::move(arg)); } +void EmbedderConfigBuilder::AddDartEntrypointArgument(std::string arg) { + if (arg.size() == 0) { + return; + } + + dart_entrypoint_arguments_.emplace_back(std::move(arg)); +} + void EmbedderConfigBuilder::SetPlatformTaskRunner( const FlutterTaskRunnerDescription* runner) { if (runner == nullptr) { @@ -317,6 +325,23 @@ UniqueEngine EmbedderConfigBuilder::SetupEngine(bool run) const { project_args.command_line_argc = 0; } + std::vector dart_args; + dart_args.reserve(dart_entrypoint_arguments_.size()); + + for (const auto& arg : dart_entrypoint_arguments_) { + dart_args.push_back(arg.c_str()); + } + + if (dart_args.size() > 0) { + project_args.dart_entrypoint_argv = dart_args.data(); + project_args.dart_entrypoint_argc = dart_args.size(); + } else { + // Clear it out in case this is not the first engine launch from the + // embedder config builder. + project_args.dart_entrypoint_argv = nullptr; + project_args.dart_entrypoint_argc = 0; + } + auto result = run ? FlutterEngineRun(FLUTTER_ENGINE_VERSION, &renderer_config_, &project_args, &context_, &engine) diff --git a/shell/platform/embedder/tests/embedder_config_builder.h b/shell/platform/embedder/tests/embedder_config_builder.h index 29dc4ef65ad11..4c37d644bc7ac 100644 --- a/shell/platform/embedder/tests/embedder_config_builder.h +++ b/shell/platform/embedder/tests/embedder_config_builder.h @@ -76,6 +76,8 @@ class EmbedderConfigBuilder { void AddCommandLineArgument(std::string arg); + void AddDartEntrypointArgument(std::string arg); + void SetPlatformTaskRunner(const FlutterTaskRunnerDescription* runner); void SetRenderTaskRunner(const FlutterTaskRunnerDescription* runner); @@ -106,6 +108,7 @@ class EmbedderConfigBuilder { FlutterCustomTaskRunners custom_task_runners_ = {}; FlutterCompositor compositor_ = {}; std::vector command_line_arguments_; + std::vector dart_entrypoint_arguments_; UniqueEngine SetupEngine(bool run) const; diff --git a/shell/platform/embedder/tests/embedder_unittests.cc b/shell/platform/embedder/tests/embedder_unittests.cc index 786b7da063e9f..538ab9ae15dad 100644 --- a/shell/platform/embedder/tests/embedder_unittests.cc +++ b/shell/platform/embedder/tests/embedder_unittests.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #define FML_USED_ON_EMBEDDER @@ -473,6 +472,33 @@ TEST_F(EmbedderTest, VMShutsDownWhenNoEnginesInProcess) { } } +//------------------------------------------------------------------------------ +/// +TEST_F(EmbedderTest, DartEntrypointArgs) { + auto& context = GetEmbedderContext(ContextType::kSoftwareContext); + EmbedderConfigBuilder builder(context); + builder.SetSoftwareRendererConfig(); + builder.AddDartEntrypointArgument("foo"); + builder.AddDartEntrypointArgument("bar"); + builder.SetDartEntrypoint("dart_entrypoint_args"); + fml::AutoResetWaitableEvent callback_latch; + std::vector callback_args; + auto nativeArgumentsCallback = [&callback_args, + &callback_latch](Dart_NativeArguments args) { + Dart_Handle exception = nullptr; + callback_args = + tonic::DartConverter>::FromArguments( + args, 0, exception); + callback_latch.Signal(); + }; + context.AddNativeCallback("NativeArgumentsCallback", + CREATE_NATIVE_ENTRY(nativeArgumentsCallback)); + auto engine = builder.LaunchEngine(); + callback_latch.Wait(); + ASSERT_EQ(callback_args[0], "foo"); + ASSERT_EQ(callback_args[1], "bar"); +} + //------------------------------------------------------------------------------ /// These snapshots may be materialized from symbols and the size field may not /// be relevant. Since this information is redundant, engine launch should not diff --git a/shell/platform/embedder/tests/embedder_unittests_gl.cc b/shell/platform/embedder/tests/embedder_unittests_gl.cc index 5c6b160d601d5..5bb104fae97cf 100644 --- a/shell/platform/embedder/tests/embedder_unittests_gl.cc +++ b/shell/platform/embedder/tests/embedder_unittests_gl.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #define FML_USED_ON_EMBEDDER diff --git a/shell/platform/embedder/tests/embedder_unittests_util.cc b/shell/platform/embedder/tests/embedder_unittests_util.cc index 78bdae8441a55..5058e2c30815e 100644 --- a/shell/platform/embedder/tests/embedder_unittests_util.cc +++ b/shell/platform/embedder/tests/embedder_unittests_util.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #define FML_USED_ON_EMBEDDER diff --git a/shell/platform/fuchsia/BUILD.gn b/shell/platform/fuchsia/BUILD.gn index 45dc143af8aed..cf4493e51797d 100644 --- a/shell/platform/fuchsia/BUILD.gn +++ b/shell/platform/fuchsia/BUILD.gn @@ -10,11 +10,9 @@ import("//flutter/tools/fuchsia/dart.gni") import("//flutter/tools/fuchsia/fuchsia_host_bundle.gni") product_suffix = "" -is_product = false if (flutter_runtime_mode == "release") { product_suffix = "product_" - is_product = true } fuchsia_host_bundle("flutter_binaries") { @@ -35,7 +33,7 @@ fuchsia_host_bundle("dart_binaries") { name = "dart_binaries" _gen_snapshot_to_use = gen_snapshot + "($host_toolchain)" - if (is_product) { + if (flutter_runtime_mode == "release") { _gen_snapshot_to_use = gen_snapshot_product + "($host_toolchain)" } diff --git a/shell/platform/fuchsia/dart-pkg/zircon/sdk_ext/system.cc b/shell/platform/fuchsia/dart-pkg/zircon/sdk_ext/system.cc index 1c3d642a2c987..9482c453d50ca 100644 --- a/shell/platform/fuchsia/dart-pkg/zircon/sdk_ext/system.cc +++ b/shell/platform/fuchsia/dart-pkg/zircon/sdk_ext/system.cc @@ -98,7 +98,8 @@ Dart_Handle MakeHandleList(const std::vector& in_handles) { tonic::DartClassLibrary& class_library = tonic::DartState::Current()->class_library(); Dart_Handle handle_type = class_library.GetClass("zircon", "Handle"); - Dart_Handle list = Dart_NewListOfType(handle_type, in_handles.size()); + Dart_Handle list = Dart_NewListOfTypeFilled( + handle_type, Handle::CreateInvalid(), in_handles.size()); if (Dart_IsError(list)) return list; for (size_t i = 0; i < in_handles.size(); i++) { diff --git a/shell/platform/fuchsia/dart/compiler.dart b/shell/platform/fuchsia/dart/compiler.dart index c955e218812c8..b0ab6f2db0b34 100644 --- a/shell/platform/fuchsia/dart/compiler.dart +++ b/shell/platform/fuchsia/dart/compiler.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// @dart = 2.9 + import 'dart:async'; import 'dart:io'; diff --git a/shell/platform/fuchsia/dart_runner/BUILD.gn b/shell/platform/fuchsia/dart_runner/BUILD.gn index 4e3f514608a3a..442ff4111c9f0 100644 --- a/shell/platform/fuchsia/dart_runner/BUILD.gn +++ b/shell/platform/fuchsia/dart_runner/BUILD.gn @@ -93,8 +93,8 @@ runner("dart_jit_product_runner_bin") { product = true extra_defines = [ "DART_PRODUCT" ] extra_deps = [ - "//third_party/dart/runtime:libdart_jit_product", - "//third_party/dart/runtime/platform:libdart_platform_jit_product", + "//third_party/dart/runtime:libdart_jit", + "//third_party/dart/runtime/platform:libdart_platform_jit", ] } @@ -121,8 +121,8 @@ runner("dart_aot_product_runner_bin") { ] extra_deps = [ "embedder:dart_aot_product_snapshot_cc", - "//third_party/dart/runtime:libdart_precompiled_runtime_product", - "//third_party/dart/runtime/platform:libdart_platform_precompiled_runtime_product", + "//third_party/dart/runtime:libdart_precompiled_runtime", + "//third_party/dart/runtime/platform:libdart_platform_precompiled_runtime", ] } diff --git a/shell/platform/fuchsia/dart_runner/dart_runner.cc b/shell/platform/fuchsia/dart_runner/dart_runner.cc index 959803f5a8f19..76d85405ab474 100644 --- a/shell/platform/fuchsia/dart_runner/dart_runner.cc +++ b/shell/platform/fuchsia/dart_runner/dart_runner.cc @@ -36,9 +36,8 @@ namespace { const char* kDartVMArgs[] = { // clang-format off - // TODO(FL-117): Re-enable causal async stack traces when this issue is - // addressed. "--no_causal_async_stacks", + "--lazy_async_stacks", "--systrace_timeline", "--timeline_streams=Compiler,Dart,Debugger,Embedder,GC,Isolate,VM", @@ -47,10 +46,6 @@ const char* kDartVMArgs[] = { "--precompilation", #else "--enable_mirrors=false", - - // The interpreter is enabled unconditionally. If an app is built for - // debugging (that is, with no bytecode), the VM will fall back on ASTs. - "--enable_interpreter", #endif // No asserts in debug or release product. diff --git a/shell/platform/fuchsia/dart_runner/embedder/BUILD.gn b/shell/platform/fuchsia/dart_runner/embedder/BUILD.gn index c237b971677ff..edbf3e4506ecd 100644 --- a/shell/platform/fuchsia/dart_runner/embedder/BUILD.gn +++ b/shell/platform/fuchsia/dart_runner/embedder/BUILD.gn @@ -47,6 +47,7 @@ template("create_aot_snapshot") { args = [ "--no_causal_async_stacks", + "--lazy_async_stacks", "--deterministic", "--snapshot_kind=vm-aot-assembly", "--assembly=" + rebase_path(snapshot_assembly), diff --git a/shell/platform/fuchsia/dart_runner/kernel/BUILD.gn b/shell/platform/fuchsia/dart_runner/kernel/BUILD.gn index 7bab46d61592f..11f777402e138 100644 --- a/shell/platform/fuchsia/dart_runner/kernel/BUILD.gn +++ b/shell/platform/fuchsia/dart_runner/kernel/BUILD.gn @@ -22,10 +22,6 @@ compile_platform("kernel_platform_files") { args = [ "--enable-experiment=non-nullable", "--nnbd-agnostic", - - # TODO(dartbug.com/36342): enable bytecode for core libraries when performance of bytecode - # pipeline is on par with default pipeline and continuously tracked. - # "--bytecode", "--target=dart_runner", "dart:core", ] @@ -72,10 +68,8 @@ template("create_kernel_core_snapshot") { tool = gen_snapshot_to_use args = [ - # TODO(FL-117): Re-enable causal async stack traces when this issue is - # addressed. "--no_causal_async_stacks", - "--use_bytecode_compiler", + "--lazy_async_stacks", "--enable_mirrors=false", "--deterministic", "--snapshot_kind=core-jit", diff --git a/shell/platform/fuchsia/dart_runner/vmservice/BUILD.gn b/shell/platform/fuchsia/dart_runner/vmservice/BUILD.gn index 186feedd2d392..ce44912e7dea8 100644 --- a/shell/platform/fuchsia/dart_runner/vmservice/BUILD.gn +++ b/shell/platform/fuchsia/dart_runner/vmservice/BUILD.gn @@ -58,6 +58,7 @@ template("aot_snapshot") { args = [ "--no_causal_async_stacks", + "--lazy_async_stacks", "--deterministic", "--snapshot_kind=app-aot-elf", "--elf=" + rebase_path(snapshot_path), diff --git a/shell/platform/fuchsia/flutter/BUILD.gn b/shell/platform/fuchsia/flutter/BUILD.gn index 69b312f441573..6b38a9136f490 100644 --- a/shell/platform/fuchsia/flutter/BUILD.gn +++ b/shell/platform/fuchsia/flutter/BUILD.gn @@ -229,8 +229,8 @@ flutter_runner("jit_product") { product = true extra_deps = [ - "//third_party/dart/runtime:libdart_jit_product", - "//third_party/dart/runtime/platform:libdart_platform_jit_product", + "//third_party/dart/runtime:libdart_jit", + "//third_party/dart/runtime/platform:libdart_platform_jit", ] } @@ -249,8 +249,8 @@ flutter_runner("aot_product") { product = true extra_deps = [ - "//third_party/dart/runtime:libdart_precompiled_runtime_product", - "//third_party/dart/runtime/platform:libdart_platform_precompiled_runtime_product", + "//third_party/dart/runtime:libdart_precompiled_runtime", + "//third_party/dart/runtime/platform:libdart_platform_precompiled_runtime", ] } diff --git a/shell/platform/fuchsia/flutter/collect_traces.dart b/shell/platform/fuchsia/flutter/collect_traces.dart index cfb67e09f788c..ed017218e1771 100644 --- a/shell/platform/fuchsia/flutter/collect_traces.dart +++ b/shell/platform/fuchsia/flutter/collect_traces.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// @dart = 2.9 + import 'dart:io'; import 'dart:convert'; diff --git a/shell/platform/fuchsia/flutter/component.cc b/shell/platform/fuchsia/flutter/component.cc index f317d7b988d5e..8544031f162f9 100644 --- a/shell/platform/fuchsia/flutter/component.cc +++ b/shell/platform/fuchsia/flutter/component.cc @@ -402,20 +402,7 @@ Application::Application( settings_.task_observer_remove = std::bind( &CurrentMessageLoopRemoveAfterTaskObserver, std::placeholders::_1); - // TODO(FL-117): Re-enable causal async stack traces when this issue is - // addressed. - settings_.dart_flags = {"--no_causal_async_stacks"}; - - // Disable code collection as it interferes with JIT code warmup - // by decreasing usage counters and flushing code which is still useful. - settings_.dart_flags.push_back("--no-collect_code"); - - if (!flutter::DartVM::IsRunningPrecompiledCode()) { - // The interpreter is enabled unconditionally in JIT mode. If an app is - // built for debugging (that is, with no bytecode), the VM will fall back on - // ASTs. - settings_.dart_flags.push_back("--enable_interpreter"); - } + settings_.dart_flags = {"--no_causal_async_stacks", "--lazy_async_stacks"}; // Don't collect CPU samples from Dart VM C++ code. settings_.dart_flags.push_back("--no_profile_vm"); @@ -494,7 +481,7 @@ class FileInNamespaceBuffer final : public fml::Mapping { } uintptr_t addr; zx_status_t status = - zx::vmar::root_self()->map(0, buffer.vmo, 0, buffer.size, flags, &addr); + zx::vmar::root_self()->map(flags, 0, buffer.vmo, 0, buffer.size, &addr); if (status != ZX_OK) { FML_LOG(FATAL) << "Failed to map " << path << ": " << zx_status_get_string(status); @@ -617,8 +604,8 @@ void Application::OnEngineTerminate(const Engine* shell_holder) { // terminate when the last shell goes away. The error code return to the // application controller will be the last isolate that had an error. auto return_code = shell_holder->GetEngineReturnCode(); - if (return_code.first) { - last_return_code_ = return_code; + if (return_code.has_value()) { + last_return_code_ = {true, return_code.value()}; } shell_holders_.erase(found); diff --git a/shell/platform/fuchsia/flutter/compositor_context.cc b/shell/platform/fuchsia/flutter/compositor_context.cc index 599ebf77b0dd2..005a9cca0705b 100644 --- a/shell/platform/fuchsia/flutter/compositor_context.cc +++ b/shell/platform/fuchsia/flutter/compositor_context.cc @@ -14,7 +14,7 @@ namespace flutter_runner { class ScopedFrame final : public flutter::CompositorContext::ScopedFrame { public: ScopedFrame(CompositorContext& context, - GrContext* gr_context, + GrDirectContext* gr_context, SkCanvas* canvas, flutter::ExternalViewEmbedder* view_embedder, const SkMatrix& root_surface_transformation, diff --git a/shell/platform/fuchsia/flutter/engine.cc b/shell/platform/fuchsia/flutter/engine.cc index 3380726840ad6..67af712aadb64 100644 --- a/shell/platform/fuchsia/flutter/engine.cc +++ b/shell/platform/fuchsia/flutter/engine.cc @@ -400,11 +400,11 @@ Engine::~Engine() { } } -std::pair Engine::GetEngineReturnCode() const { - std::pair code(false, 0); +std::optional Engine::GetEngineReturnCode() const { if (!shell_) { - return code; + return std::nullopt; } + std::optional code; fml::AutoResetWaitableEvent latch; fml::TaskRunner::RunNowOrPostTask( shell_->GetTaskRunners().GetUITaskRunner(), diff --git a/shell/platform/fuchsia/flutter/engine.h b/shell/platform/fuchsia/flutter/engine.h index 20e14e849fc17..4fc29e7cfac10 100644 --- a/shell/platform/fuchsia/flutter/engine.h +++ b/shell/platform/fuchsia/flutter/engine.h @@ -5,6 +5,8 @@ #ifndef FLUTTER_SHELL_PLATFORM_FUCHSIA_ENGINE_H_ #define FLUTTER_SHELL_PLATFORM_FUCHSIA_ENGINE_H_ +#include + #include #include #include @@ -55,7 +57,7 @@ class Engine final { // Returns the Dart return code for the root isolate if one is present. This // call is thread safe and synchronous. This call must be made infrequently. - std::pair GetEngineReturnCode() const; + std::optional GetEngineReturnCode() const; #if !defined(DART_PRODUCT) void WriteProfileToTrace() const; diff --git a/shell/platform/fuchsia/flutter/flutter_runner_product_configuration.cc b/shell/platform/fuchsia/flutter/flutter_runner_product_configuration.cc index c4f91e31925cd..bc2a49cf1f9e5 100644 --- a/shell/platform/fuchsia/flutter/flutter_runner_product_configuration.cc +++ b/shell/platform/fuchsia/flutter/flutter_runner_product_configuration.cc @@ -3,6 +3,7 @@ // found in the LICENSE file. #include "flutter_runner_product_configuration.h" +#include #include "rapidjson/document.h" @@ -17,15 +18,26 @@ FlutterRunnerProductConfiguration::FlutterRunnerProductConfiguration( return; // Parse out all values we're expecting. - if (auto& val = document["vsync_offset_in_us"]; val.IsInt()) { - vsync_offset_ = fml::TimeDelta::FromMicroseconds(val.GetInt()); + if (document.HasMember("vsync_offset_in_us")) { + auto& val = document["vsync_offset_in_us"]; + if (val.IsInt()) + vsync_offset_ = fml::TimeDelta::FromMicroseconds(val.GetInt()); } - if (auto& val = document["max_frames_in_flight"]; val.IsInt()) { - max_frames_in_flight_ = val.GetInt(); + if (document.HasMember("max_frames_in_flight")) { + auto& val = document["max_frames_in_flight"]; + if (val.IsInt()) + max_frames_in_flight_ = val.GetInt(); + } + if (document.HasMember("intercept_all_input")) { + auto& val = document["intercept_all_input"]; + if (val.IsBool()) + intercept_all_input_ = val.GetBool(); } #if defined(LEGACY_FUCHSIA_EMBEDDER) - if (auto& val = document["use_legacy_renderer"]; val.IsBool()) { - use_legacy_renderer_ = val.GetBool(); + if (document.HasMember("use_legacy_renderer")) { + auto& val = document["use_legacy_renderer"]; + if (val.IsBool()) + use_legacy_renderer_ = val.GetBool(); } #endif } diff --git a/shell/platform/fuchsia/flutter/flutter_runner_product_configuration.h b/shell/platform/fuchsia/flutter/flutter_runner_product_configuration.h index 355f365042ef0..792f9fe25cd37 100644 --- a/shell/platform/fuchsia/flutter/flutter_runner_product_configuration.h +++ b/shell/platform/fuchsia/flutter/flutter_runner_product_configuration.h @@ -16,6 +16,7 @@ class FlutterRunnerProductConfiguration { fml::TimeDelta get_vsync_offset() { return vsync_offset_; } uint64_t get_max_frames_in_flight() { return max_frames_in_flight_; } + bool get_intercept_all_input() { return intercept_all_input_; } #if defined(LEGACY_FUCHSIA_EMBEDDER) bool use_legacy_renderer() { return use_legacy_renderer_; } #endif @@ -23,6 +24,7 @@ class FlutterRunnerProductConfiguration { private: fml::TimeDelta vsync_offset_ = fml::TimeDelta::Zero(); uint64_t max_frames_in_flight_ = 3; + bool intercept_all_input_ = false; #if defined(LEGACY_FUCHSIA_EMBEDDER) bool use_legacy_renderer_ = true; #endif diff --git a/shell/platform/fuchsia/flutter/kernel/BUILD.gn b/shell/platform/fuchsia/flutter/kernel/BUILD.gn index f221f4af1977c..1d1f45bc8c846 100644 --- a/shell/platform/fuchsia/flutter/kernel/BUILD.gn +++ b/shell/platform/fuchsia/flutter/kernel/BUILD.gn @@ -22,10 +22,6 @@ compile_platform("kernel_platform_files") { args = [ "--enable-experiment=non-nullable", "--nnbd-agnostic", - - # TODO(dartbug.com/36342): enable bytecode for core libraries when performance of bytecode - # pipeline is on par with default pipeline and continuously tracked. - # "--bytecode", "--target=flutter_runner", "dart:core", ] @@ -76,10 +72,8 @@ template("core_snapshot") { tool = gen_snapshot_to_use args = [ - # TODO(FL-117): Re-enable causal async stack traces when this issue is - # addressed. "--no_causal_async_stacks", - "--use_bytecode_compiler", + "--lazy_async_stacks", "--enable_mirrors=false", "--deterministic", "--snapshot_kind=core-jit", diff --git a/shell/platform/fuchsia/flutter/kernel/extract_far.dart b/shell/platform/fuchsia/flutter/kernel/extract_far.dart index 4bfd72a3e462e..442f64be4bd3d 100644 --- a/shell/platform/fuchsia/flutter/kernel/extract_far.dart +++ b/shell/platform/fuchsia/flutter/kernel/extract_far.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// @dart = 2.9 + import "dart:io"; import "package:args/args.dart"; diff --git a/shell/platform/fuchsia/flutter/runner.cc b/shell/platform/fuchsia/flutter/runner.cc index 1e8e26dfbfdeb..43b7da3e3db0a 100644 --- a/shell/platform/fuchsia/flutter/runner.cc +++ b/shell/platform/fuchsia/flutter/runner.cc @@ -45,9 +45,9 @@ uintptr_t GetICUData(const fuchsia::mem::Buffer& icu_data) { return 0u; uintptr_t data = 0u; - zx_status_t status = zx::vmar::root_self()->map( - 0, icu_data.vmo, 0, static_cast(data_size), ZX_VM_PERM_READ, - &data); + zx_status_t status = + zx::vmar::root_self()->map(ZX_VM_PERM_READ, 0, icu_data.vmo, 0, + static_cast(data_size), &data); if (status == ZX_OK) { return data; } diff --git a/shell/platform/fuchsia/flutter/tests/flutter_runner_product_configuration_unittests.cc b/shell/platform/fuchsia/flutter/tests/flutter_runner_product_configuration_unittests.cc index 1cdcd4a0a5353..2e287dbbdbf65 100644 --- a/shell/platform/fuchsia/flutter/tests/flutter_runner_product_configuration_unittests.cc +++ b/shell/platform/fuchsia/flutter/tests/flutter_runner_product_configuration_unittests.cc @@ -88,4 +88,26 @@ TEST_F(FlutterRunnerProductConfigurationTest, MissingMaxFramesInFlight) { minimum_reasonable_max_frames_in_flight); } +TEST_F(FlutterRunnerProductConfigurationTest, ValidInterceptAllInput) { + const std::string json_string = "{ \"intercept_all_input\" : true } "; + const uint64_t expected_intercept_all_input = true; + + FlutterRunnerProductConfiguration product_config = + FlutterRunnerProductConfiguration(json_string); + + EXPECT_EQ(expected_intercept_all_input, + product_config.get_intercept_all_input()); +} + +TEST_F(FlutterRunnerProductConfigurationTest, MissingInterceptAllInput) { + const std::string json_string = "{ \"intercept_all_input\" : } "; + const uint64_t expected_intercept_all_input = false; + + FlutterRunnerProductConfiguration product_config = + FlutterRunnerProductConfiguration(json_string); + + EXPECT_EQ(expected_intercept_all_input, + product_config.get_intercept_all_input()); +} + } // namespace flutter_runner_test diff --git a/shell/platform/fuchsia/flutter/vulkan_surface.cc b/shell/platform/fuchsia/flutter/vulkan_surface.cc index ba697d915348f..a28b4dad7293c 100644 --- a/shell/platform/fuchsia/flutter/vulkan_surface.cc +++ b/shell/platform/fuchsia/flutter/vulkan_surface.cc @@ -344,6 +344,8 @@ bool VulkanSurface::SetupSkiaSurface(sk_sp context, image_info.fImageTiling = image_create_info.tiling; image_info.fImageLayout = image_create_info.initialLayout; image_info.fFormat = image_create_info.format; + image_info.fImageUsageFlags = image_create_info.usage; + image_info.fSampleCount = 1; image_info.fLevelCount = image_create_info.mipLevels; GrBackendRenderTarget sk_render_target(size.width(), size.height(), 0, diff --git a/shell/platform/fuchsia/flutter/vulkan_surface_producer.cc b/shell/platform/fuchsia/flutter/vulkan_surface_producer.cc index 36eeadd85afa3..ed69ce441eac5 100644 --- a/shell/platform/fuchsia/flutter/vulkan_surface_producer.cc +++ b/shell/platform/fuchsia/flutter/vulkan_surface_producer.cc @@ -160,7 +160,7 @@ void VulkanSurfaceProducer::OnSurfacesPresented( // Do a single flush for all canvases derived from the context. { - TRACE_EVENT0("flutter", "GrContext::flushAndSignalSemaphores"); + TRACE_EVENT0("flutter", "GrDirectContext::flushAndSignalSemaphores"); context_->flushAndSubmit(); } diff --git a/shell/platform/fuchsia/runtime/dart/profiler_symbols/dart_profiler_symbols.dart b/shell/platform/fuchsia/runtime/dart/profiler_symbols/dart_profiler_symbols.dart index 0e6af0f48e93f..c0436c096ab34 100644 --- a/shell/platform/fuchsia/runtime/dart/profiler_symbols/dart_profiler_symbols.dart +++ b/shell/platform/fuchsia/runtime/dart/profiler_symbols/dart_profiler_symbols.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// @dart = 2.9 + // On Fuchsia, in lieu of the ELF dynamic symbol table consumed through dladdr, // the Dart VM profiler consumes symbols produced by this tool, which have the // format diff --git a/shell/platform/fuchsia/runtime/dart/utils/mapped_resource.cc b/shell/platform/fuchsia/runtime/dart/utils/mapped_resource.cc index a30afe538bb5d..9adba9308c545 100644 --- a/shell/platform/fuchsia/runtime/dart/utils/mapped_resource.cc +++ b/shell/platform/fuchsia/runtime/dart/utils/mapped_resource.cc @@ -82,8 +82,8 @@ bool MappedResource::LoadFromVmo(const std::string& path, flags |= ZX_VM_PERM_EXECUTE; } uintptr_t addr; - zx_status_t status = zx::vmar::root_self()->map( - 0, resource_vmo.vmo, 0, resource_vmo.size, flags, &addr); + zx_status_t status = zx::vmar::root_self()->map(flags, 0, resource_vmo.vmo, 0, + resource_vmo.size, &addr); if (status != ZX_OK) { FX_LOGF(ERROR, LOG_TAG, "Failed to map: %s", zx_status_get_string(status)); return false; diff --git a/shell/platform/glfw/client_wrapper/BUILD.gn b/shell/platform/glfw/client_wrapper/BUILD.gn index 3a4f57263ef6f..c0d6ca643896f 100644 --- a/shell/platform/glfw/client_wrapper/BUILD.gn +++ b/shell/platform/glfw/client_wrapper/BUILD.gn @@ -76,6 +76,7 @@ executable("client_wrapper_glfw_unittests") { "flutter_engine_unittests.cc", "flutter_window_controller_unittests.cc", "flutter_window_unittests.cc", + "plugin_registrar_glfw_unittests.cc", ] defines = [ "FLUTTER_DESKTOP_LIBRARY" ] diff --git a/shell/platform/glfw/client_wrapper/include/flutter/plugin_registrar_glfw.h b/shell/platform/glfw/client_wrapper/include/flutter/plugin_registrar_glfw.h index 1d0a83e20b2f9..2459dde60bf29 100644 --- a/shell/platform/glfw/client_wrapper/include/flutter/plugin_registrar_glfw.h +++ b/shell/platform/glfw/client_wrapper/include/flutter/plugin_registrar_glfw.h @@ -26,7 +26,12 @@ class PluginRegistrarGlfw : public PluginRegistrar { FlutterDesktopPluginRegistrarGetWindow(core_registrar)); } - virtual ~PluginRegistrarGlfw() = default; + virtual ~PluginRegistrarGlfw() { + // Must be the first call. + ClearPlugins(); + // Explicitly cleared to facilitate destruction order testing. + window_.reset(); + } // Prevent copying. PluginRegistrarGlfw(PluginRegistrarGlfw const&) = delete; diff --git a/shell/platform/glfw/client_wrapper/plugin_registrar_glfw_unittests.cc b/shell/platform/glfw/client_wrapper/plugin_registrar_glfw_unittests.cc new file mode 100644 index 0000000000000..ff95921fb35e3 --- /dev/null +++ b/shell/platform/glfw/client_wrapper/plugin_registrar_glfw_unittests.cc @@ -0,0 +1,60 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include + +#include "flutter/shell/platform/glfw/client_wrapper/include/flutter/plugin_registrar_glfw.h" +#include "flutter/shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.h" +#include "gtest/gtest.h" + +namespace flutter { + +namespace { + +// A test plugin that tries to access registrar state during destruction and +// reports it out via a flag provided at construction. +class TestPlugin : public Plugin { + public: + // registrar_valid_at_destruction will be set at destruction to indicate + // whether or not |registrar->window()| was non-null. + TestPlugin(PluginRegistrarGlfw* registrar, + bool* registrar_valid_at_destruction) + : registrar_(registrar), + registrar_valid_at_destruction_(registrar_valid_at_destruction) {} + virtual ~TestPlugin() { + *registrar_valid_at_destruction_ = registrar_->window() != nullptr; + } + + private: + PluginRegistrarGlfw* registrar_; + bool* registrar_valid_at_destruction_; +}; + +} // namespace + +TEST(PluginRegistrarGlfwTest, GetView) { + testing::ScopedStubFlutterGlfwApi scoped_api_stub( + std::make_unique()); + PluginRegistrarGlfw registrar( + reinterpret_cast(1)); + EXPECT_NE(registrar.window(), nullptr); +} + +// Tests that the registrar runs plugin destructors before its own teardown. +TEST(PluginRegistrarGlfwTest, PluginDestroyedBeforeRegistrar) { + auto dummy_registrar_handle = + reinterpret_cast(1); + bool registrar_valid_at_destruction = false; + { + PluginRegistrarGlfw registrar(dummy_registrar_handle); + + auto plugin = std::make_unique(®istrar, + ®istrar_valid_at_destruction); + registrar.AddPlugin(std::move(plugin)); + } + EXPECT_TRUE(registrar_valid_at_destruction); +} + +} // namespace flutter diff --git a/shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.cc b/shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.cc index c12e54fd4dad7..df61b34550715 100644 --- a/shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.cc +++ b/shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.cc @@ -183,6 +183,12 @@ FlutterDesktopPluginRegistrarRef FlutterDesktopGetPluginRegistrar( return reinterpret_cast(2); } +FlutterDesktopWindowRef FlutterDesktopPluginRegistrarGetWindow( + FlutterDesktopPluginRegistrarRef registrar) { + // The stub ignores this, so just return an arbitrary non-zero value. + return reinterpret_cast(3); +} + void FlutterDesktopPluginRegistrarEnableInputBlocking( FlutterDesktopPluginRegistrarRef registrar, const char* channel) { diff --git a/shell/platform/glfw/flutter_glfw.cc b/shell/platform/glfw/flutter_glfw.cc index 703048a4626a9..ee26ea2452f4a 100644 --- a/shell/platform/glfw/flutter_glfw.cc +++ b/shell/platform/glfw/flutter_glfw.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #include "flutter/shell/platform/glfw/public/flutter_glfw.h" diff --git a/shell/platform/glfw/key_event_handler.cc b/shell/platform/glfw/key_event_handler.cc index cdc5deb2bbee1..bdd121091f54e 100644 --- a/shell/platform/glfw/key_event_handler.cc +++ b/shell/platform/glfw/key_event_handler.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #include "flutter/shell/platform/glfw/key_event_handler.h" diff --git a/shell/platform/glfw/text_input_plugin.cc b/shell/platform/glfw/text_input_plugin.cc index 7a7a88f0ccd57..7ebc29ecef6be 100644 --- a/shell/platform/glfw/text_input_plugin.cc +++ b/shell/platform/glfw/text_input_plugin.cc @@ -195,7 +195,7 @@ void TextInputPlugin::HandleMethodCall( base = extent = 0; } active_model_->SetText(text->value.GetString()); - active_model_->SetSelection(base, extent); + active_model_->SetSelection(TextRange(base, extent)); } else { result->NotImplemented(); return; @@ -210,14 +210,14 @@ void TextInputPlugin::SendStateUpdate(const TextInputModel& model) { auto& allocator = args->GetAllocator(); args->PushBack(client_id_, allocator); + TextRange selection = model.selection(); rapidjson::Value editing_state(rapidjson::kObjectType); editing_state.AddMember(kComposingBaseKey, -1, allocator); editing_state.AddMember(kComposingExtentKey, -1, allocator); editing_state.AddMember(kSelectionAffinityKey, kAffinityDownstream, allocator); - editing_state.AddMember(kSelectionBaseKey, model.selection_base(), allocator); - editing_state.AddMember(kSelectionExtentKey, model.selection_extent(), - allocator); + editing_state.AddMember(kSelectionBaseKey, selection.base(), allocator); + editing_state.AddMember(kSelectionExtentKey, selection.extent(), allocator); editing_state.AddMember(kSelectionIsDirectionalKey, false, allocator); editing_state.AddMember( kTextKey, rapidjson::Value(model.GetText(), allocator).Move(), allocator); diff --git a/shell/platform/linux/BUILD.gn b/shell/platform/linux/BUILD.gn index 523ea1194899c..31dd919bccd3d 100644 --- a/shell/platform/linux/BUILD.gn +++ b/shell/platform/linux/BUILD.gn @@ -80,7 +80,10 @@ source_set("flutter_linux_sources") { "fl_standard_message_codec_private.h", ] - configs += [ "//flutter/shell/platform/linux/config:gtk" ] + configs += [ + "//flutter/shell/platform/linux/config:gtk", + "//flutter/shell/platform/linux/config:wayland-client", + ] sources = [ "egl_utils.cc", diff --git a/shell/platform/linux/config/BUILD.gn b/shell/platform/linux/config/BUILD.gn index 77402c5315b78..a50518f973066 100644 --- a/shell/platform/linux/config/BUILD.gn +++ b/shell/platform/linux/config/BUILD.gn @@ -22,3 +22,7 @@ pkg_config("egl") { pkg_config("wayland-egl") { packages = [ "wayland-egl" ] } + +pkg_config("wayland-client") { + packages = [ "wayland-client" ] +} diff --git a/shell/platform/linux/fl_dart_project.cc b/shell/platform/linux/fl_dart_project.cc index b9c0b3623f9f6..25551c72403f9 100644 --- a/shell/platform/linux/fl_dart_project.cc +++ b/shell/platform/linux/fl_dart_project.cc @@ -19,6 +19,7 @@ struct _FlDartProject { gchar* aot_library_path; gchar* assets_path; gchar* icu_data_path; + gchar** dart_entrypoint_args; }; G_DEFINE_TYPE(FlDartProject, fl_dart_project, G_TYPE_OBJECT) @@ -42,6 +43,7 @@ static void fl_dart_project_dispose(GObject* object) { g_clear_pointer(&self->aot_library_path, g_free); g_clear_pointer(&self->assets_path, g_free); g_clear_pointer(&self->icu_data_path, g_free); + g_clear_pointer(&self->dart_entrypoint_args, g_strfreev); G_OBJECT_CLASS(fl_dart_project_parent_class)->dispose(object); } @@ -98,6 +100,20 @@ G_MODULE_EXPORT const gchar* fl_dart_project_get_icu_data_path( return self->icu_data_path; } +G_MODULE_EXPORT gchar** fl_dart_project_get_dart_entrypoint_arguments( + FlDartProject* self) { + g_return_val_if_fail(FL_IS_DART_PROJECT(self), nullptr); + return self->dart_entrypoint_args; +} + +G_MODULE_EXPORT void fl_dart_project_set_dart_entrypoint_arguments( + FlDartProject* self, + char** argv) { + g_return_if_fail(FL_IS_DART_PROJECT(self)); + g_clear_pointer(&self->dart_entrypoint_args, g_strfreev); + self->dart_entrypoint_args = g_strdupv(argv); +} + GPtrArray* fl_dart_project_get_switches(FlDartProject* self) { GPtrArray* switches = g_ptr_array_new_with_free_func(g_free); std::vector env_switches = flutter::GetSwitchesFromEnvironment(); diff --git a/shell/platform/linux/fl_engine.cc b/shell/platform/linux/fl_engine.cc index e9cb23a749d59..6a53f91442017 100644 --- a/shell/platform/linux/fl_engine.cc +++ b/shell/platform/linux/fl_engine.cc @@ -381,6 +381,9 @@ gboolean fl_engine_start(FlEngine* self, GError** error) { // so that all switches are used. g_ptr_array_insert(command_line_args, 0, g_strdup("flutter")); + gchar** dart_entrypoint_args = + fl_dart_project_get_dart_entrypoint_arguments(self->project); + FlutterProjectArgs args = {}; args.struct_size = sizeof(FlutterProjectArgs); args.assets_path = fl_dart_project_get_assets_path(self->project); @@ -391,6 +394,9 @@ gboolean fl_engine_start(FlEngine* self, GError** error) { args.platform_message_callback = fl_engine_platform_message_cb; args.custom_task_runners = &custom_task_runners; args.shutdown_dart_vm_when_done = true; + args.dart_entrypoint_argc = g_strv_length(dart_entrypoint_args); + args.dart_entrypoint_argv = + reinterpret_cast(dart_entrypoint_args); if (FlutterEngineRunsAOTCompiledDartCode()) { FlutterEngineAOTDataSource source = {}; diff --git a/shell/platform/linux/fl_json_message_codec.cc b/shell/platform/linux/fl_json_message_codec.cc index 3fbfb1238ea74..7441ebe63c645 100644 --- a/shell/platform/linux/fl_json_message_codec.cc +++ b/shell/platform/linux/fl_json_message_codec.cc @@ -2,10 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// TODO(cbracken): lint disabled due to rapidjson null deref issue. -// https://github.com/flutter/flutter/issues/65676 -// FLUTTER_NOLINT - #include "flutter/shell/platform/linux/public/flutter_linux/fl_json_message_codec.h" #include diff --git a/shell/platform/linux/fl_method_response.cc b/shell/platform/linux/fl_method_response.cc index 5bd1d85252e12..cd66bdf23ab2d 100644 --- a/shell/platform/linux/fl_method_response.cc +++ b/shell/platform/linux/fl_method_response.cc @@ -26,8 +26,11 @@ struct _FlMethodNotImplementedResponse { FlMethodResponse parent_instance; }; -// Added here to stop the compiler from optimising this function away. +// Added here to stop the compiler from optimising these functions away. G_MODULE_EXPORT GType fl_method_response_get_type(); +G_MODULE_EXPORT GType fl_method_success_response_get_type(); +G_MODULE_EXPORT GType fl_method_error_response_get_type(); +G_MODULE_EXPORT GType fl_method_not_implemented_response_get_type(); G_DEFINE_TYPE(FlMethodResponse, fl_method_response, G_TYPE_OBJECT) G_DEFINE_TYPE(FlMethodSuccessResponse, @@ -97,16 +100,19 @@ G_MODULE_EXPORT FlValue* fl_method_response_get_result(FlMethodResponse* self, FlValue* details = fl_method_error_response_get_details(FL_METHOD_ERROR_RESPONSE(self)); g_autofree gchar* details_text = nullptr; - if (details != nullptr) + if (details != nullptr) { details_text = fl_value_to_string(details); + } g_autoptr(GString) error_message = g_string_new(""); g_string_append_printf(error_message, "Remote code returned error %s", code); - if (message != nullptr) + if (message != nullptr) { g_string_append_printf(error_message, ": %s", message); - if (details_text != nullptr) + } + if (details_text != nullptr) { g_string_append_printf(error_message, " %s", details_text); + } g_set_error_literal(error, FL_METHOD_RESPONSE_ERROR, FL_METHOD_RESPONSE_ERROR_REMOTE_ERROR, error_message->str); @@ -128,8 +134,9 @@ G_MODULE_EXPORT FlMethodSuccessResponse* fl_method_success_response_new( FlMethodSuccessResponse* self = FL_METHOD_SUCCESS_RESPONSE( g_object_new(fl_method_success_response_get_type(), nullptr)); - if (result != nullptr) + if (result != nullptr) { self->result = fl_value_ref(result); + } return self; } diff --git a/shell/platform/linux/fl_renderer_x11.cc b/shell/platform/linux/fl_renderer_x11.cc index 5fa6e57978bb5..2c646b16ca223 100644 --- a/shell/platform/linux/fl_renderer_x11.cc +++ b/shell/platform/linux/fl_renderer_x11.cc @@ -3,6 +3,8 @@ // found in the LICENSE file. #include "fl_renderer_x11.h" +#ifdef GDK_WINDOWING_X11 + #include "flutter/shell/platform/linux/egl_utils.h" struct _FlRendererX11 { @@ -108,3 +110,5 @@ static void fl_renderer_x11_init(FlRendererX11* self) {} FlRendererX11* fl_renderer_x11_new() { return FL_RENDERER_X11(g_object_new(fl_renderer_x11_get_type(), nullptr)); } + +#endif // GDK_WINDOWING_X11 diff --git a/shell/platform/linux/fl_renderer_x11.h b/shell/platform/linux/fl_renderer_x11.h index 8c32dcd0bc31b..d8d3b60f25a6c 100644 --- a/shell/platform/linux/fl_renderer_x11.h +++ b/shell/platform/linux/fl_renderer_x11.h @@ -5,6 +5,9 @@ #ifndef FLUTTER_SHELL_PLATFORM_LINUX_FL_RENDERER_X11_H_ #define FLUTTER_SHELL_PLATFORM_LINUX_FL_RENDERER_X11_H_ +#include + +#ifdef GDK_WINDOWING_X11 #include #include "flutter/shell/platform/linux/fl_renderer.h" @@ -35,4 +38,6 @@ FlRendererX11* fl_renderer_x11_new(); G_END_DECLS +#endif // GDK_WINDOWING_X11 + #endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_RENDERER_X11_H_ diff --git a/shell/platform/linux/fl_text_input_plugin.cc b/shell/platform/linux/fl_text_input_plugin.cc index ae9ed10a98897..b89e184238c9d 100644 --- a/shell/platform/linux/fl_text_input_plugin.cc +++ b/shell/platform/linux/fl_text_input_plugin.cc @@ -22,6 +22,9 @@ static constexpr char kHideMethod[] = "TextInput.hide"; static constexpr char kUpdateEditingStateMethod[] = "TextInputClient.updateEditingState"; static constexpr char kPerformActionMethod[] = "TextInputClient.performAction"; +static constexpr char kSetEditableSizeAndTransform[] = + "TextInput.setEditableSizeAndTransform"; +static constexpr char kSetMarkedTextRect[] = "TextInput.setMarkedTextRect"; static constexpr char kInputActionKey[] = "inputAction"; static constexpr char kTextInputTypeKey[] = "inputType"; @@ -34,6 +37,8 @@ static constexpr char kSelectionIsDirectionalKey[] = "selectionIsDirectional"; static constexpr char kComposingBaseKey[] = "composingBase"; static constexpr char kComposingExtentKey[] = "composingExtent"; +static constexpr char kTransform[] = "transform"; + static constexpr char kTextAffinityDownstream[] = "TextAffinity.downstream"; static constexpr char kMultilineInputType[] = "TextInputType.multiline"; @@ -57,6 +62,19 @@ struct _FlTextInputPlugin { GtkIMContext* im_context; flutter::TextInputModel* text_model; + + // The owning Flutter view. + FlView* view; + + // A 4x4 matrix that maps from `EditableText` local coordinates to the + // coordinate system of `PipelineOwner.rootNode`. + double editabletext_transform[4][4]; + + // The smallest rect, in local coordinates, of the text in the composing + // range, or of the caret in the case where there is no current composing + // range. This value is updated via `TextInput.setMarkedTextRect` messages + // over the text input channel. + GdkRectangle composing_rect; }; G_DEFINE_TYPE(FlTextInputPlugin, fl_text_input_plugin, G_TYPE_OBJECT) @@ -90,23 +108,31 @@ static void update_editing_state(FlTextInputPlugin* self) { fl_value_append_take(args, fl_value_new_int(self->client_id)); g_autoptr(FlValue) value = fl_value_new_map(); + TextRange selection = self->text_model->selection(); fl_value_set_string_take( value, kTextKey, fl_value_new_string(self->text_model->GetText().c_str())); - fl_value_set_string_take( - value, kSelectionBaseKey, - fl_value_new_int(self->text_model->selection_base())); - fl_value_set_string_take( - value, kSelectionExtentKey, - fl_value_new_int(self->text_model->selection_extent())); + fl_value_set_string_take(value, kSelectionBaseKey, + fl_value_new_int(selection.base())); + fl_value_set_string_take(value, kSelectionExtentKey, + fl_value_new_int(selection.extent())); + + int composing_base = self->text_model->composing() + ? self->text_model->composing_range().base() + : -1; + int composing_extent = self->text_model->composing() + ? self->text_model->composing_range().extent() + : -1; + fl_value_set_string_take(value, kComposingBaseKey, + fl_value_new_int(composing_base)); + fl_value_set_string_take(value, kComposingExtentKey, + fl_value_new_int(composing_extent)); // The following keys are not implemented and set to default values. fl_value_set_string_take(value, kSelectionAffinityKey, fl_value_new_string(kTextAffinityDownstream)); fl_value_set_string_take(value, kSelectionIsDirectionalKey, fl_value_new_bool(FALSE)); - fl_value_set_string_take(value, kComposingBaseKey, fl_value_new_int(-1)); - fl_value_set_string_take(value, kComposingExtentKey, fl_value_new_int(-1)); fl_value_append(args, value); @@ -139,9 +165,41 @@ static void perform_action(FlTextInputPlugin* self) { nullptr, perform_action_response_cb, self); } +// Signal handler for GtkIMContext::preedit-start +static void im_preedit_start_cb(FlTextInputPlugin* self) { + self->text_model->BeginComposing(); + + // Set the top-level window used for system input method windows. + GdkWindow* window = + gtk_widget_get_window(gtk_widget_get_toplevel(GTK_WIDGET(self->view))); + gtk_im_context_set_client_window(self->im_context, window); +} + +// Signal handler for GtkIMContext::preedit-changed +static void im_preedit_changed_cb(FlTextInputPlugin* self) { + g_autofree gchar* buf = nullptr; + gint cursor_offset = 0; + gtk_im_context_get_preedit_string(self->im_context, &buf, nullptr, + &cursor_offset); + cursor_offset += self->text_model->composing_range().base(); + self->text_model->UpdateComposingText(buf); + self->text_model->SetSelection(TextRange(cursor_offset, cursor_offset)); + + update_editing_state(self); +} + // Signal handler for GtkIMContext::commit static void im_commit_cb(FlTextInputPlugin* self, const gchar* text) { self->text_model->AddText(text); + if (self->text_model->composing()) { + self->text_model->CommitComposing(); + } + update_editing_state(self); +} + +// Signal handler for GtkIMContext::preedit-end +static void im_preedit_end_cb(FlTextInputPlugin* self) { + self->text_model->EndComposing(); update_editing_state(self); } @@ -184,12 +242,13 @@ static FlMethodResponse* set_client(FlTextInputPlugin* self, FlValue* args) { // Clear the multiline flag, then set it only if the field is multiline. self->input_multiline = FALSE; FlValue* input_type_value = - fl_value_lookup(config_value, fl_value_new_string(kTextInputTypeKey)); + fl_value_lookup_string(config_value, kTextInputTypeKey); if (fl_value_get_type(input_type_value) == FL_VALUE_TYPE_MAP) { - FlValue* input_type_name = fl_value_lookup( - input_type_value, fl_value_new_string(kTextInputTypeNameKey)); - if (fl_value_equal(input_type_name, - fl_value_new_string(kMultilineInputType))) { + FlValue* input_type_name = + fl_value_lookup_string(input_type_value, kTextInputTypeNameKey); + if (fl_value_get_type(input_type_name) == FL_VALUE_TYPE_STRING && + g_strcmp0(fl_value_get_string(input_type_name), kMultilineInputType) == + 0) { self->input_multiline = TRUE; } } @@ -209,6 +268,8 @@ static FlMethodResponse* set_editing_state(FlTextInputPlugin* self, FlValue* args) { const gchar* text = fl_value_get_string(fl_value_lookup_string(args, kTextKey)); + self->text_model->SetText(text); + int64_t selection_base = fl_value_get_int(fl_value_lookup_string(args, kSelectionBaseKey)); int64_t selection_extent = @@ -219,7 +280,20 @@ static FlMethodResponse* set_editing_state(FlTextInputPlugin* self, } self->text_model->SetText(text); - self->text_model->SetSelection(selection_base, selection_extent); + self->text_model->SetSelection(TextRange(selection_base, selection_extent)); + + int64_t composing_base = + fl_value_get_int(fl_value_lookup_string(args, kComposingBaseKey)); + int64_t composing_extent = + fl_value_get_int(fl_value_lookup_string(args, kComposingExtentKey)); + if (composing_base == -1 && composing_extent == -1) { + self->text_model->EndComposing(); + } else { + size_t composing_start = std::min(composing_base, composing_extent); + size_t cursor_offset = selection_base - composing_start; + self->text_model->SetComposingRange( + TextRange(composing_base, composing_extent), cursor_offset); + } return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } @@ -238,6 +312,83 @@ static FlMethodResponse* hide(FlTextInputPlugin* self) { return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } +// Update the IM cursor position. +// +// As text is input by the user, the framework sends two streams of updates +// over the text input channel: updates to the composing rect (cursor rect when +// not in IME composing mode) and updates to the matrix transform from local +// coordinates to Flutter root coordinates. This function is called after each +// of these updates. It transforms the composing rect to GTK window coordinates +// and notifies GTK of the updated cursor position. +static void update_im_cursor_position(FlTextInputPlugin* self) { + // Skip update if not composing to avoid setting to position 0. + if (!self->text_model->composing()) { + return; + } + + // Transform the x, y positions of the cursor from local coordinates to + // Flutter view coordinates. + gint x = self->composing_rect.x * self->editabletext_transform[0][0] + + self->composing_rect.y * self->editabletext_transform[1][0] + + self->editabletext_transform[3][0] + self->composing_rect.width; + gint y = self->composing_rect.x * self->editabletext_transform[0][1] + + self->composing_rect.y * self->editabletext_transform[1][1] + + self->editabletext_transform[3][1] + self->composing_rect.height; + + // Transform from Flutter view coordinates to GTK window coordinates. + GdkRectangle preedit_rect; + gtk_widget_translate_coordinates( + GTK_WIDGET(self->view), gtk_widget_get_toplevel(GTK_WIDGET(self->view)), + x, y, &preedit_rect.x, &preedit_rect.y); + + // Set the cursor location in window coordinates so that GTK can position any + // system input method windows. + gtk_im_context_set_cursor_location(self->im_context, &preedit_rect); +} + +// Handles updates to the EditableText size and position from the framework. +// +// On changes to the size or position of the RenderObject underlying the +// EditableText, this update may be triggered. It provides an updated size and +// transform from the local coordinate system of the EditableText to root +// Flutter coordinate system. +static FlMethodResponse* set_editable_size_and_transform( + FlTextInputPlugin* self, + FlValue* args) { + FlValue* transform = fl_value_lookup_string(args, kTransform); + size_t transform_len = fl_value_get_length(transform); + g_warn_if_fail(transform_len == 16); + + for (size_t i = 0; i < transform_len; ++i) { + double val = fl_value_get_float(fl_value_get_list_value(transform, i)); + self->editabletext_transform[i / 4][i % 4] = val; + } + update_im_cursor_position(self); + + return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); +} + +// Handles updates to the composing rect from the framework. +// +// On changes to the state of the EditableText in the framework, this update +// may be triggered. It provides an updated rect for the composing region in +// local coordinates of the EditableText. In the case where there is no +// composing region, the cursor rect is sent. +static FlMethodResponse* set_marked_text_rect(FlTextInputPlugin* self, + FlValue* args) { + self->composing_rect.x = + fl_value_get_float(fl_value_lookup_string(args, "x")); + self->composing_rect.y = + fl_value_get_float(fl_value_lookup_string(args, "y")); + self->composing_rect.width = + fl_value_get_float(fl_value_lookup_string(args, "width")); + self->composing_rect.height = + fl_value_get_float(fl_value_lookup_string(args, "height")); + update_im_cursor_position(self); + + return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); +} + // Called when a method call is received from Flutter. static void method_call_cb(FlMethodChannel* channel, FlMethodCall* method_call, @@ -258,6 +409,10 @@ static void method_call_cb(FlMethodChannel* channel, response = clear_client(self); } else if (strcmp(method, kHideMethod) == 0) { response = hide(self); + } else if (strcmp(method, kSetEditableSizeAndTransform) == 0) { + response = set_editable_size_and_transform(self, args); + } else if (strcmp(method, kSetMarkedTextRect) == 0) { + response = set_marked_text_rect(self, args); } else { response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); } @@ -268,6 +423,11 @@ static void method_call_cb(FlMethodChannel* channel, } } +static void view_weak_notify_cb(gpointer user_data, GObject* object) { + FlTextInputPlugin* self = FL_TEXT_INPUT_PLUGIN(object); + self->view = nullptr; +} + static void fl_text_input_plugin_dispose(GObject* object) { FlTextInputPlugin* self = FL_TEXT_INPUT_PLUGIN(object); @@ -290,6 +450,15 @@ static void fl_text_input_plugin_init(FlTextInputPlugin* self) { self->client_id = kClientIdUnset; self->im_context = gtk_im_multicontext_new(); self->input_multiline = FALSE; + g_signal_connect_object(self->im_context, "preedit-start", + G_CALLBACK(im_preedit_start_cb), self, + G_CONNECT_SWAPPED); + g_signal_connect_object(self->im_context, "preedit-end", + G_CALLBACK(im_preedit_end_cb), self, + G_CONNECT_SWAPPED); + g_signal_connect_object(self->im_context, "preedit-changed", + G_CALLBACK(im_preedit_changed_cb), self, + G_CONNECT_SWAPPED); g_signal_connect_object(self->im_context, "commit", G_CALLBACK(im_commit_cb), self, G_CONNECT_SWAPPED); g_signal_connect_object(self->im_context, "retrieve-surrounding", @@ -301,7 +470,8 @@ static void fl_text_input_plugin_init(FlTextInputPlugin* self) { self->text_model = new flutter::TextInputModel(); } -FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger) { +FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger, + FlView* view) { g_return_val_if_fail(FL_IS_BINARY_MESSENGER(messenger), nullptr); FlTextInputPlugin* self = FL_TEXT_INPUT_PLUGIN( @@ -312,6 +482,8 @@ FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger) { fl_method_channel_new(messenger, kChannelName, FL_METHOD_CODEC(codec)); fl_method_channel_set_method_call_handler(self->channel, method_call_cb, self, nullptr); + self->view = view; + g_object_weak_ref(G_OBJECT(view), view_weak_notify_cb, self); return self; } diff --git a/shell/platform/linux/fl_text_input_plugin.h b/shell/platform/linux/fl_text_input_plugin.h index 14ab5cfc04417..d68f5903c17a6 100644 --- a/shell/platform/linux/fl_text_input_plugin.h +++ b/shell/platform/linux/fl_text_input_plugin.h @@ -8,6 +8,7 @@ #include #include "flutter/shell/platform/linux/public/flutter_linux/fl_binary_messenger.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_view.h" G_BEGIN_DECLS @@ -27,13 +28,15 @@ G_DECLARE_FINAL_TYPE(FlTextInputPlugin, /** * fl_text_input_plugin_new: * @messenger: an #FlBinaryMessenger. + * @view: the #FlView with which the text input plugin is associated. * * Creates a new plugin that implements SystemChannels.textInput from the * Flutter services library. * * Returns: a new #FlTextInputPlugin. */ -FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger); +FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger, + FlView* view); /** * fl_text_input_plugin_filter_keypress diff --git a/shell/platform/linux/fl_view.cc b/shell/platform/linux/fl_view.cc index d38ef5aa5aa63..b65e3da7d4297 100644 --- a/shell/platform/linux/fl_view.cc +++ b/shell/platform/linux/fl_view.cc @@ -5,7 +5,9 @@ #include "flutter/shell/platform/linux/public/flutter_linux/fl_view.h" #include +#ifdef GDK_WINDOWING_X11 #include +#endif #include #include "flutter/shell/platform/linux/fl_engine_private.h" @@ -131,17 +133,27 @@ static void fl_view_plugin_registry_iface_init( iface->get_registrar_for_plugin = fl_view_get_registrar_for_plugin; } +static FlRenderer* fl_view_get_renderer_for_display(GdkDisplay* display) { +#ifdef GDK_WINDOWING_X11 + if (GDK_IS_X11_DISPLAY(display)) { + return FL_RENDERER(fl_renderer_x11_new()); + } +#endif + + if (GDK_IS_WAYLAND_DISPLAY(display)) { + return FL_RENDERER(fl_renderer_wayland_new()); + } + + g_error("Unsupported GDK backend"); + + return nullptr; +} + static void fl_view_constructed(GObject* object) { FlView* self = FL_VIEW(object); GdkDisplay* display = gtk_widget_get_display(GTK_WIDGET(self)); - if (GDK_IS_X11_DISPLAY(display)) { - self->renderer = FL_RENDERER(fl_renderer_x11_new()); - } else if (GDK_IS_WAYLAND_DISPLAY(display)) { - self->renderer = FL_RENDERER(fl_renderer_wayland_new()); - } else { - g_error("Unsupported GDK backend"); - } + self->renderer = fl_view_get_renderer_for_display(display); self->engine = fl_engine_new(self->project, self->renderer); // Create system channel handlers. @@ -149,7 +161,7 @@ static void fl_view_constructed(GObject* object) { self->key_event_plugin = fl_key_event_plugin_new(messenger); self->mouse_cursor_plugin = fl_mouse_cursor_plugin_new(messenger, self); self->platform_plugin = fl_platform_plugin_new(messenger); - self->text_input_plugin = fl_text_input_plugin_new(messenger); + self->text_input_plugin = fl_text_input_plugin_new(messenger, self); } static void fl_view_set_property(GObject* object, diff --git a/shell/platform/linux/public/flutter_linux/fl_dart_project.h b/shell/platform/linux/public/flutter_linux/fl_dart_project.h index 3a0e91f245133..1b56e24c32627 100644 --- a/shell/platform/linux/public/flutter_linux/fl_dart_project.h +++ b/shell/platform/linux/public/flutter_linux/fl_dart_project.h @@ -96,6 +96,30 @@ const gchar* fl_dart_project_get_assets_path(FlDartProject* project); */ const gchar* fl_dart_project_get_icu_data_path(FlDartProject* project); +/** + * fl_dart_project_set_dart_entrypoint_arguments: + * @project: an #FlDartProject. + * @argv: a pointer to a NULL-terminated array of C strings containing the + * command line arguments. + * + * Sets the command line arguments to be passed through to the Dart + * entrypoint function. + */ +void fl_dart_project_set_dart_entrypoint_arguments(FlDartProject* project, + char** argv); + +/** + * fl_dart_project_get_dart_entrypoint_arguments: + * @project: an #FlDartProject. + * + * Gets the command line arguments to be passed through to the Dart entrypoint + * function. + * + * Returns: a NULL-terminated array of strings containing the command line + * arguments to be passed to the Dart entrypoint. + */ +gchar** fl_dart_project_get_dart_entrypoint_arguments(FlDartProject* project); + G_END_DECLS #endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_DART_PROJECT_H_ diff --git a/shell/platform/windows/client_wrapper/include/flutter/plugin_registrar_windows.h b/shell/platform/windows/client_wrapper/include/flutter/plugin_registrar_windows.h index 20b00aeb605eb..98093e1b396e7 100644 --- a/shell/platform/windows/client_wrapper/include/flutter/plugin_registrar_windows.h +++ b/shell/platform/windows/client_wrapper/include/flutter/plugin_registrar_windows.h @@ -36,7 +36,12 @@ class PluginRegistrarWindows : public PluginRegistrar { FlutterDesktopPluginRegistrarGetView(core_registrar)); } - virtual ~PluginRegistrarWindows() = default; + virtual ~PluginRegistrarWindows() { + // Must be the first call. + ClearPlugins(); + // Explicitly cleared to facilitate destruction order testing. + view_.reset(); + } // Prevent copying. PluginRegistrarWindows(PluginRegistrarWindows const&) = delete; diff --git a/shell/platform/windows/client_wrapper/plugin_registrar_windows_unittests.cc b/shell/platform/windows/client_wrapper/plugin_registrar_windows_unittests.cc index 63049a5eb82b9..0c9bdb9618907 100644 --- a/shell/platform/windows/client_wrapper/plugin_registrar_windows_unittests.cc +++ b/shell/platform/windows/client_wrapper/plugin_registrar_windows_unittests.cc @@ -43,6 +43,25 @@ class TestWindowsApi : public testing::StubFlutterWindowsApi { void* last_registered_user_data_ = nullptr; }; +// A test plugin that tries to access registrar state during destruction and +// reports it out via a flag provided at construction. +class TestPlugin : public Plugin { + public: + // registrar_valid_at_destruction will be set at destruction to indicate + // whether or not |registrar->GetView()| was non-null. + TestPlugin(PluginRegistrarWindows* registrar, + bool* registrar_valid_at_destruction) + : registrar_(registrar), + registrar_valid_at_destruction_(registrar_valid_at_destruction) {} + virtual ~TestPlugin() { + *registrar_valid_at_destruction_ = registrar_->GetView() != nullptr; + } + + private: + PluginRegistrarWindows* registrar_; + bool* registrar_valid_at_destruction_; +}; + } // namespace TEST(PluginRegistrarWindowsTest, GetView) { @@ -54,6 +73,21 @@ TEST(PluginRegistrarWindowsTest, GetView) { EXPECT_NE(registrar.GetView(), nullptr); } +// Tests that the registrar runs plugin destructors before its own teardown. +TEST(PluginRegistrarWindowsTest, PluginDestroyedBeforeRegistrar) { + auto dummy_registrar_handle = + reinterpret_cast(1); + bool registrar_valid_at_destruction = false; + { + PluginRegistrarWindows registrar(dummy_registrar_handle); + + auto plugin = std::make_unique(®istrar, + ®istrar_valid_at_destruction); + registrar.AddPlugin(std::move(plugin)); + } + EXPECT_TRUE(registrar_valid_at_destruction); +} + TEST(PluginRegistrarWindowsTest, RegisterUnregister) { testing::ScopedStubFlutterWindowsApi scoped_api_stub( std::make_unique()); diff --git a/shell/platform/windows/text_input_plugin.cc b/shell/platform/windows/text_input_plugin.cc index 1687664b42fdd..e69caf504e364 100644 --- a/shell/platform/windows/text_input_plugin.cc +++ b/shell/platform/windows/text_input_plugin.cc @@ -196,7 +196,7 @@ void TextInputPlugin::HandleMethodCall( base = extent = 0; } active_model_->SetText(text->value.GetString()); - active_model_->SetSelection(base, extent); + active_model_->SetSelection(TextRange(base, extent)); } else { result->NotImplemented(); return; @@ -211,14 +211,14 @@ void TextInputPlugin::SendStateUpdate(const TextInputModel& model) { auto& allocator = args->GetAllocator(); args->PushBack(client_id_, allocator); + TextRange selection = model.selection(); rapidjson::Value editing_state(rapidjson::kObjectType); editing_state.AddMember(kComposingBaseKey, -1, allocator); editing_state.AddMember(kComposingExtentKey, -1, allocator); editing_state.AddMember(kSelectionAffinityKey, kAffinityDownstream, allocator); - editing_state.AddMember(kSelectionBaseKey, model.selection_base(), allocator); - editing_state.AddMember(kSelectionExtentKey, model.selection_extent(), - allocator); + editing_state.AddMember(kSelectionBaseKey, selection.base(), allocator); + editing_state.AddMember(kSelectionExtentKey, selection.extent(), allocator); editing_state.AddMember(kSelectionIsDirectionalKey, false, allocator); editing_state.AddMember( kTextKey, rapidjson::Value(model.GetText(), allocator).Move(), allocator); diff --git a/shell/testing/tester_main.cc b/shell/testing/tester_main.cc index 0f0264ce9ebdb..8c970d59e22b9 100644 --- a/shell/testing/tester_main.cc +++ b/shell/testing/tester_main.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #define FML_USED_ON_EMBEDDER @@ -192,10 +191,11 @@ int RunTester(const flutter::Settings& settings, auto asset_manager = std::make_shared(); asset_manager->PushBack(std::make_unique( - fml::Duplicate(settings.assets_dir))); - asset_manager->PushBack( - std::make_unique(fml::OpenDirectory( - settings.assets_path.c_str(), false, fml::FilePermission::kRead))); + fml::Duplicate(settings.assets_dir), true)); + asset_manager->PushBack(std::make_unique( + fml::OpenDirectory(settings.assets_path.c_str(), false, + fml::FilePermission::kRead), + true)); RunConfiguration run_configuration(std::move(isolate_configuration), std::move(asset_manager)); diff --git a/shell/version/version.cc b/shell/version/version.cc index 54ff4cb07babd..77baa13107c71 100644 --- a/shell/version/version.cc +++ b/shell/version/version.cc @@ -1,7 +1,8 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT + +// FLUTTER_NOLINT: https://github.com/flutter/flutter/issues/68332 #include "flutter/shell/version/version.h" diff --git a/sky/packages/sky_engine/LICENSE b/sky/packages/sky_engine/LICENSE index 14714e389cd88..1bd8c4561a569 100644 --- a/sky/packages/sky_engine/LICENSE +++ b/sky/packages/sky_engine/LICENSE @@ -4503,13 +4503,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- dart -Copyright 2009 The Go Authors. All rights reserved. -Use of this source code is governed by a BSD-style -license that can be found in the LICENSE file --------------------------------------------------------------------------------- -dart - -Copyright 2012, the Dart project authors. +Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +for details. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are @@ -4536,10 +4531,14 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- dart -wasmer -Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file -for details. All rights reserved. +Copyright 2009 The Go Authors. All rights reserved. +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file +-------------------------------------------------------------------------------- +dart + +Copyright 2012, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are diff --git a/testing/dart_isolate_runner.cc b/testing/dart_isolate_runner.cc index 1515f2e5afb15..43d77c0165c28 100644 --- a/testing/dart_isolate_runner.cc +++ b/testing/dart_isolate_runner.cc @@ -4,6 +4,8 @@ #include "flutter/testing/dart_isolate_runner.h" +#include "flutter/runtime/isolate_configuration.h" + namespace flutter { namespace testing { AutoIsolateShutdown::AutoIsolateShutdown(std::shared_ptr isolate, @@ -47,55 +49,29 @@ AutoIsolateShutdown::~AutoIsolateShutdown() { return true; } -void RunDartCodeInIsolate(DartVMRef& vm_ref, - std::unique_ptr& result, - const Settings& settings, - const TaskRunners& task_runners, - std::string entrypoint, - const std::vector& args, - const std::string& fixtures_path, - fml::WeakPtr io_manager) { +std::unique_ptr RunDartCodeInIsolateOnUITaskRunner( + DartVMRef& vm_ref, + const Settings& p_settings, + const TaskRunners& task_runners, + std::string entrypoint, + const std::vector& args, + const std::string& fixtures_path, + fml::WeakPtr io_manager) { FML_CHECK(task_runners.GetUITaskRunner()->RunsTasksOnCurrentThread()); if (!vm_ref) { - return; + return nullptr; } auto vm_data = vm_ref.GetVMData(); if (!vm_data) { - return; + return nullptr; } - auto weak_isolate = DartIsolate::CreateRootIsolate( - vm_data->GetSettings(), // settings - vm_data->GetIsolateSnapshot(), // isolate snapshot - std::move(task_runners), // task runners - nullptr, // window - {}, // snapshot delegate - {}, // hint freed delegate - io_manager, // io manager - {}, // unref queue - {}, // image decoder - "main.dart", // advisory uri - "main", // advisory entrypoint - nullptr, // flags - settings.isolate_create_callback, // isolate create callback - settings.isolate_shutdown_callback // isolate shutdown callback - ); - - auto root_isolate = std::make_unique( - weak_isolate.lock(), task_runners.GetUITaskRunner()); - - if (!root_isolate->IsValid()) { - FML_LOG(ERROR) << "Could not create isolate."; - return; - } + auto settings = p_settings; - if (root_isolate->get()->GetPhase() != DartIsolate::Phase::LibrariesSetup) { - FML_LOG(ERROR) << "Created isolate is in unexpected phase."; - return; - } + settings.dart_entrypoint_args = args; if (!DartVM::IsRunningPrecompiledCode()) { auto kernel_file_path = @@ -103,7 +79,7 @@ void RunDartCodeInIsolate(DartVMRef& vm_ref, if (!fml::IsFile(kernel_file_path)) { FML_LOG(ERROR) << "Could not locate kernel file."; - return; + return nullptr; } auto kernel_file = fml::OpenFile(kernel_file_path.c_str(), false, @@ -111,46 +87,56 @@ void RunDartCodeInIsolate(DartVMRef& vm_ref, if (!kernel_file.is_valid()) { FML_LOG(ERROR) << "Kernel file descriptor was invalid."; - return; + return nullptr; } auto kernel_mapping = std::make_unique(kernel_file); if (kernel_mapping->GetMapping() == nullptr) { FML_LOG(ERROR) << "Could not setup kernel mapping."; - return; - } - - if (!root_isolate->get()->PrepareForRunningFromKernel( - std::move(kernel_mapping))) { - FML_LOG(ERROR) - << "Could not prepare to run the isolate from the kernel file."; - return; + return nullptr; } - } else { - if (!root_isolate->get()->PrepareForRunningFromPrecompiledCode()) { - FML_LOG(ERROR) - << "Could not prepare to run the isolate from precompiled code."; - return; - } - } - if (root_isolate->get()->GetPhase() != DartIsolate::Phase::Ready) { - FML_LOG(ERROR) << "Isolate is in unexpected phase."; - return; + settings.application_kernels = fml::MakeCopyable( + [kernel_mapping = std::move(kernel_mapping)]() mutable -> Mappings { + Mappings mappings; + mappings.emplace_back(std::move(kernel_mapping)); + return mappings; + }); } - if (!root_isolate->get()->Run(entrypoint, args, - settings.root_isolate_create_callback)) { - FML_LOG(ERROR) << "Could not run the method \"" << entrypoint - << "\" in the isolate."; - return; + auto isolate_configuration = + IsolateConfiguration::InferFromSettings(settings); + + auto isolate = + DartIsolate::CreateRunningRootIsolate( + settings, // settings + vm_data->GetIsolateSnapshot(), // isolate snapshot + std::move(task_runners), // task runners + nullptr, // window + {}, // snapshot delegate + {}, // hint freed delegate + io_manager, // io manager + {}, // unref queue + {}, // image decoder + "main.dart", // advisory uri + entrypoint.c_str(), // advisory entrypoint + DartIsolate::Flags{}, // flags + settings.isolate_create_callback, // isolate create callback + settings.isolate_shutdown_callback, // isolate shutdown callback + entrypoint, // entrypoint + std::nullopt, // library + std::move(isolate_configuration) // isolate configuration + ) + .lock(); + + if (!isolate) { + FML_LOG(ERROR) << "Could not create running isolate."; + return nullptr; } - root_isolate->get()->AddIsolateShutdownCallback( - settings.root_isolate_shutdown_callback); - - result = std::move(root_isolate); + return std::make_unique(isolate, + task_runners.GetUITaskRunner()); } std::unique_ptr RunDartCodeInIsolate( @@ -165,8 +151,9 @@ std::unique_ptr RunDartCodeInIsolate( fml::AutoResetWaitableEvent latch; fml::TaskRunner::RunNowOrPostTask( task_runners.GetUITaskRunner(), fml::MakeCopyable([&]() mutable { - RunDartCodeInIsolate(vm_ref, result, settings, task_runners, entrypoint, - args, fixtures_path, io_manager); + result = RunDartCodeInIsolateOnUITaskRunner( + vm_ref, settings, task_runners, entrypoint, args, fixtures_path, + io_manager); latch.Signal(); })); latch.Wait(); diff --git a/testing/fixture_test.cc b/testing/fixture_test.cc index 06cb2227fdfeb..3a8a94c138805 100644 --- a/testing/fixture_test.cc +++ b/testing/fixture_test.cc @@ -40,10 +40,16 @@ void FixtureTest::SetSnapshotsAndAssets(Settings& settings) { if (DartVM::IsRunningPrecompiledCode()) { FML_CHECK(PrepareSettingsForAOTWithSymbols(settings, aot_symbols_)); } else { - settings.application_kernels = [this]() { + settings.application_kernels = [this]() -> Mappings { std::vector> kernel_mappings; - kernel_mappings.emplace_back( - fml::FileMapping::CreateReadOnly(assets_dir_, "kernel_blob.bin")); + auto kernel_mapping = + fml::FileMapping::CreateReadOnly(assets_dir_, "kernel_blob.bin"); + if (!kernel_mapping || !kernel_mapping->IsValid()) { + FML_LOG(ERROR) << "Could not find kernel blob for test fixture not " + "running in precompiled mode."; + return kernel_mappings; + } + kernel_mappings.emplace_back(std::move(kernel_mapping)); return kernel_mappings; }; } diff --git a/testing/fuchsia/run_tests.sh b/testing/fuchsia/run_tests.sh index f39233dec6642..28fba45e1f8bf 100755 --- a/testing/fuchsia/run_tests.sh +++ b/testing/fuchsia/run_tests.sh @@ -45,20 +45,12 @@ fuchsia_ctl() { } reboot() { - fuchsia_ctl ssh \ - --timeout-seconds $ssh_timeout_seconds \ - --identity-file $pkey \ - -c "log_listener --dump_logs yes --file /tmp/log.txt" # As we are not using recipes we don't have a way to know the location # to upload the log to isolated. We are saving the log to a file to avoid dart # hanging when running the process and then just using printing the content to # the console. - fuchsia_ctl ssh \ - --timeout-seconds $ssh_timeout_seconds \ - --identity-file $pkey \ - -c "cat /tmp/log.txt" - - + kill -9 $PID + cat /tmp/log.txt echo "$(date) START:REBOOT ----------------------------------------" # note: this will set an exit code of 255, which we can ignore. fuchsia_ctl ssh \ @@ -82,6 +74,9 @@ for i in {1..10}; do --identity-file $pkey \ -c "echo up" && break || sleep 15; done +rm -rf /tmp/log.txt +fuchsia_ctl ssh --timeout-seconds 1800 --identity-file $pkey -c "log_listener" --log-file /tmp/log.txt & +PID=$! echo "$(date) END:WAIT_DEVICE_READY ---------------------------------" echo "$(date) START:EXTRACT_PACKAGES -------------------------------" diff --git a/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj b/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj index eeaef513460a9..dde07daccd170 100644 --- a/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj +++ b/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj @@ -27,6 +27,19 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + 24EBCE9E2534C400008D1687 /* Embed Libraries */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 12; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Libraries"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 0AC232F424BA71D300A85907 /* SemanticsObjectTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SemanticsObjectTest.mm; sourceTree = ""; }; 0AC232F724BA71D300A85907 /* FlutterEngineTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterEngineTest.mm; sourceTree = ""; }; @@ -158,8 +171,10 @@ buildConfigurationList = 0D6AB6D222BB05E200EEE540 /* Build configuration list for PBXNativeTarget "IosUnitTests" */; buildPhases = ( 0D6AB6AD22BB05E100EEE540 /* Sources */, + 242904382534E32900F90C97 /* ShellScript */, 0D6AB6AE22BB05E100EEE540 /* Frameworks */, 0D6AB6AF22BB05E100EEE540 /* Resources */, + 24EBCE9E2534C400008D1687 /* Embed Libraries */, ); buildRules = ( ); @@ -246,6 +261,26 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 242904382534E32900F90C97 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "set -ex\nxcode_frameworks_dir=\"${TARGET_BUILD_DIR}/${PlugIns/IosUnitTestsTests.xctest/Frameworks/}\"\nmkdir -p \"${xcode_frameworks_dir}\"\nflutter_engine_rel_path=\"../../../../out/${FLUTTER_ENGINE}\"\ncp -f \"${flutter_engine_rel_path}/libios_test_flutter.dylib\" \"${xcode_frameworks_dir}\"\ncp -f \"${flutter_engine_rel_path}/libocmock_shared.dylib\" \"${xcode_frameworks_dir}\"\nif [[ -n \"${EXPANDED_CODE_SIGN_IDENTITY:-}\" ]]; then\n codesign --force --verbose --sign \"${EXPANDED_CODE_SIGN_IDENTITY}\" -- \"${xcode_frameworks_dir}/libocmock_shared.dylib\"\n codesign --force --verbose --sign \"${EXPANDED_CODE_SIGN_IDENTITY}\" -- \"${xcode_frameworks_dir}/libios_test_flutter.dylib\"\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 0D6AB6AD22BB05E100EEE540 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -475,9 +510,11 @@ ../../../../out/$FLUTTER_ENGINE, ../../../../out/$FLUTTER_ENGINE/obj/flutter/shell/platform/darwin/ios/, ../../../../out/$FLUTTER_ENGINE/obj/third_party/ocmock/, + "$(PROJECT_DIR)", ); OTHER_LDFLAGS = ( - "-locmock", + "-L../../../../out/$FLUTTER_ENGINE", + "-locmock_shared", "-ObjC", "-lios_test_flutter", ); @@ -517,9 +554,11 @@ ../../../../out/$FLUTTER_ENGINE, ../../../../out/$FLUTTER_ENGINE/obj/flutter/shell/platform/darwin/ios/, ../../../../out/$FLUTTER_ENGINE/obj/third_party/ocmock/, + "$(PROJECT_DIR)", ); OTHER_LDFLAGS = ( - "-locmock", + "-L../../../../out/$FLUTTER_ENGINE", + "-locmock_shared", "-ObjC", "-lios_test_flutter", ); diff --git a/testing/mock_canvas.cc b/testing/mock_canvas.cc index 8b2ba26601cda..76da0a5eabed2 100644 --- a/testing/mock_canvas.cc +++ b/testing/mock_canvas.cc @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// FLUTTER_NOLINT #include "flutter/testing/mock_canvas.h" diff --git a/testing/run_tests.py b/testing/run_tests.py index a11f91c47b1e8..cfcedd74f7816 100755 --- a/testing/run_tests.py +++ b/testing/run_tests.py @@ -147,6 +147,7 @@ def RunCCTests(build_dir, filter): # These unit-tests are Objective-C and can only run on Darwin. if IsMac(): RunEngineExecutable(build_dir, 'flutter_channels_unittests', filter, shuffle_flags) + RunEngineExecutable(build_dir, 'flutter_desktop_darwin_unittests', filter, shuffle_flags) # https://github.com/flutter/flutter/issues/36296 if IsLinux(): diff --git a/testing/scenario_app/compile_ios_jit.sh b/testing/scenario_app/compile_ios_jit.sh index 6c072066bfad3..7cae11b446676 100755 --- a/testing/scenario_app/compile_ios_jit.sh +++ b/testing/scenario_app/compile_ios_jit.sh @@ -74,7 +74,8 @@ echo "Compiling JIT Snapshot..." "$DEVICE_TOOLS/gen_snapshot" --deterministic \ --enable-asserts \ - --causal_async_stacks \ + --no-causal_async_stacks \ + --lazy_async_stacks \ --isolate_snapshot_instructions="$OUTDIR/isolate_snapshot_instr" \ --snapshot_kind=app-jit \ --load_vm_snapshot_data="$DEVICE_TOOLS/../gen/flutter/lib/snapshot/vm_isolate_snapshot.bin" \ @@ -85,12 +86,15 @@ echo "Compiling JIT Snapshot..." cp "$DEVICE_TOOLS/../gen/flutter/lib/snapshot/vm_isolate_snapshot.bin" "$OUTDIR/App.framework/flutter_assets/vm_snapshot_data" +LLVM_BIN_PATH="${SCRIPT_DIR}/../../../buildtools/mac-x64/clang/bin" SYSROOT=$(xcrun --sdk iphonesimulator --show-sdk-path) echo "Using $SYSROOT as sysroot." echo "Creating stub App using $SYSROOT..." -echo "static const int Moo = 88;" | xcrun clang -x c \ +# Use buildroot clang so we can override the linker to use in our LUCI recipe. +# See: https://github.com/flutter/flutter/issues/65901 +echo "static const int Moo = 88;" | "$LLVM_BIN_PATH/clang" -x c \ -arch x86_64 \ -fembed-bitcode-marker \ -isysroot "$SYSROOT" \ @@ -110,4 +114,3 @@ rm -rf "$SCRIPT_DIR/ios/Scenarios/App.framework" rm -rf "$SCRIPT_DIR/ios/Scenarios/Flutter.framework" cp -R "$OUTDIR/App.framework" "$SCRIPT_DIR/ios/Scenarios" cp -R "$DEVICE_TOOLS/../Flutter.framework" "$SCRIPT_DIR/ios/Scenarios" - diff --git a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj index cc4462541c2d5..71d8cb57f989d 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj +++ b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 242F37A222E636DE001E83D4 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 246B4E4522E3B61000073EBF /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 242F37A322E636DE001E83D4 /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 246B4E4122E3B5F700073EBF /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 244EA6D0230DBE8900B2D26E /* golden_platform_view_D21AP.png in Resources */ = {isa = PBXBuildFile; fileRef = 244EA6CF230DBE8900B2D26E /* golden_platform_view_D21AP.png */; }; + 246A6611252E693A00EAB0F3 /* RenderingSelectionTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 246A6610252E693A00EAB0F3 /* RenderingSelectionTest.m */; }; 246B4E4222E3B5F700073EBF /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 246B4E4122E3B5F700073EBF /* App.framework */; }; 246B4E4622E3B61000073EBF /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 246B4E4522E3B61000073EBF /* Flutter.framework */; }; 248D76CC22E388370012F0C1 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 248D76CB22E388370012F0C1 /* AppDelegate.m */; }; @@ -126,6 +127,7 @@ 0D8470A3240F0B1F0030B565 /* StatusBarTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = StatusBarTest.m; sourceTree = ""; }; 0DB781FC22EA2C0300E9B371 /* FlutterViewControllerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FlutterViewControllerTest.m; sourceTree = ""; }; 244EA6CF230DBE8900B2D26E /* golden_platform_view_D21AP.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = golden_platform_view_D21AP.png; sourceTree = ""; }; + 246A6610252E693A00EAB0F3 /* RenderingSelectionTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RenderingSelectionTest.m; sourceTree = ""; }; 246B4E4122E3B5F700073EBF /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = App.framework; sourceTree = ""; }; 246B4E4522E3B61000073EBF /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Flutter.framework; sourceTree = ""; }; 248D76C722E388370012F0C1 /* Scenarios.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Scenarios.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -291,6 +293,7 @@ 0D8470A3240F0B1F0030B565 /* StatusBarTest.m */, 0A42BFB32447E179007E212E /* TextSemanticsFocusTest.m */, 0A42BFB52447E19F007E212E /* TextSemanticsFocusTest.h */, + 246A6610252E693A00EAB0F3 /* RenderingSelectionTest.m */, ); path = ScenariosUITests; sourceTree = ""; @@ -488,6 +491,7 @@ 6816DBA42318358200A51400 /* PlatformViewGoldenTestManager.m in Sources */, 248D76EF22E388380012F0C1 /* PlatformViewUITests.m in Sources */, 0D8470A4240F0B1F0030B565 /* StatusBarTest.m in Sources */, + 246A6611252E693A00EAB0F3 /* RenderingSelectionTest.m in Sources */, 4F06F1B32473296E000AF246 /* LocalizationInitializationTest.m in Sources */, 0A42BFB42447E179007E212E /* TextSemanticsFocusTest.m in Sources */, ); diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m index b981938a05bfc..96ddd0c734f6d 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m +++ b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m @@ -50,7 +50,8 @@ - (BOOL)application:(UIApplication*)application @"--gesture-reject-eager" : @"platform_view_gesture_reject_eager", @"--gesture-accept" : @"platform_view_gesture_accept", @"--tap-status-bar" : @"tap_status_bar", - @"--text-semantics-focus" : @"text_semantics_focus" + @"--text-semantics-focus" : @"text_semantics_focus", + @"--animated-color-square" : @"animated_color_square", }; __block NSString* flutterViewControllerTestName = nil; [launchArgsMap @@ -119,6 +120,16 @@ - (void)setupFlutterViewControllerTest:(NSString*)scenarioIdentifier { gestureRecognizersBlockingPolicy: FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded]; self.window.rootViewController = flutterViewController; + + if ([[[NSProcessInfo processInfo] arguments] containsObject:@"--assert-ca-layer-type"]) { + if ([[[NSProcessInfo processInfo] arguments] containsObject:@"--enable-software-rendering"]) { + NSAssert([flutterViewController.view.layer isKindOfClass:[CALayer class]], + @"Expected CALayer for software rendering."); + } else { + NSAssert([flutterViewController.view.layer isKindOfClass:[CAMetalLayer class]], + @"Expected CAMetalLayer for non-software rendering."); + } + } } @end diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenPlatformViewTests.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenPlatformViewTests.m index 78d65163b8a5a..29f1a472aa094 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenPlatformViewTests.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenPlatformViewTests.m @@ -32,7 +32,7 @@ - (void)setUp { self.continueAfterFailure = NO; self.application = [[XCUIApplication alloc] init]; - self.application.launchArguments = @[ self.manager.launchArg ]; + self.application.launchArguments = @[ self.manager.launchArg, @"--enable-software-rendering" ]; [self.application launch]; } diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m index 6771454c40318..b37a0ad14b14b 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m @@ -18,7 +18,8 @@ - (void)setUp { - (void)testRejectPolicyUtilTouchesEnded { XCUIApplication* app = [[XCUIApplication alloc] init]; - app.launchArguments = @[ @"--gesture-reject-after-touches-ended" ]; + app.launchArguments = + @[ @"--gesture-reject-after-touches-ended", @"--enable-software-rendering" ]; [app launch]; NSPredicate* predicateToFindPlatformView = diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/RenderingSelectionTest.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/RenderingSelectionTest.m new file mode 100644 index 0000000000000..0f52816b60e5c --- /dev/null +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/RenderingSelectionTest.m @@ -0,0 +1,34 @@ +// Copyright 2020 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +@interface RenderingSelectionTest : XCTestCase +@property(nonatomic, strong) XCUIApplication* application; +@end + +@implementation RenderingSelectionTest + +- (void)setUp { + [super setUp]; + self.continueAfterFailure = NO; + self.application = [[XCUIApplication alloc] init]; +} + +- (void)testSoftwareRendering { + self.application.launchArguments = + @[ @"--animated-color-square", @"--assert-ca-layer-type", @"--enable-software-rendering" ]; + [self.application launch]; + + // App asserts that the rendering API is CALayer +} + +- (void)testMetalRendering { + self.application.launchArguments = @[ @"--animated-color-square", @"--assert-ca-layer-type" ]; + [self.application launch]; + + // App asserts that the rendering API is CAMetalLayer +} +@end diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/UnobstructedPlatformViewTests.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/UnobstructedPlatformViewTests.m index 73186b0ba00a2..fa335a338c6a5 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosUITests/UnobstructedPlatformViewTests.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/UnobstructedPlatformViewTests.m @@ -21,7 +21,8 @@ - (void)setUp { // +---+ - (void)testNoOverlay { XCUIApplication* app = [[XCUIApplication alloc] init]; - app.launchArguments = @[ @"--platform-view-no-overlay-intersection" ]; + app.launchArguments = + @[ @"--platform-view-no-overlay-intersection", @"--enable-software-rendering" ]; [app launch]; XCUIElement* platform_view = app.textViews[@"platform_view[0]"]; diff --git a/testing/scenario_app/lib/src/scenarios.dart b/testing/scenario_app/lib/src/scenarios.dart index 6aa553380d34c..cc74066771e41 100644 --- a/testing/scenario_app/lib/src/scenarios.dart +++ b/testing/scenario_app/lib/src/scenarios.dart @@ -14,7 +14,7 @@ import 'scenario.dart'; import 'send_text_focus_semantics.dart'; import 'touches_scenario.dart'; -typedef ScenarioFactory = Scenario Function(); +typedef ScenarioFactory = Scenario Function(); // ignore: public_member_api_docs int _viewId = 0; diff --git a/testing/testing.gni b/testing/testing.gni index 488310a05032a..37a8302702bd8 100644 --- a/testing/testing.gni +++ b/testing/testing.gni @@ -80,6 +80,7 @@ template("dart_snapshot_kernel") { rebase_path("$root_out_dir/flutter_patched_sdk"), "--target", "flutter", + "--enable-experiment=non-nullable", "--output-dill", rebase_path(invoker.dart_kernel, root_out_dir), "--depfile", @@ -128,7 +129,8 @@ template("dart_snapshot_aot") { outputs = [ elf_object ] args = [ - "--causal_async_stacks", + "--no-causal_async_stacks", + "--lazy_async_stacks", "--deterministic", "--snapshot_kind=app-aot-elf", "--elf=" + rebase_path(elf_object), diff --git a/third_party/tonic/BUILD.gn b/third_party/tonic/BUILD.gn index eb0136d54bd59..f5bedda8dab0d 100644 --- a/third_party/tonic/BUILD.gn +++ b/third_party/tonic/BUILD.gn @@ -11,6 +11,12 @@ config("config") { source_set("tonic") { sources = [ + "common/build_config.h", + "common/log.cc", + "common/log.h", + "common/macros.h", + "converter/dart_converter.cc", + "converter/dart_converter.h", "dart_args.h", "dart_binding_macros.h", "dart_class_library.cc", @@ -32,17 +38,53 @@ source_set("tonic") { "dart_wrappable.cc", "dart_wrappable.h", "dart_wrapper_info.h", - ] + "file_loader/file_loader.cc", + "file_loader/file_loader.h", + "logging/dart_error.cc", + "logging/dart_error.h", + "logging/dart_invoke.cc", + "logging/dart_invoke.h", + "scopes/dart_api_scope.h", + "scopes/dart_isolate_scope.cc", + "scopes/dart_isolate_scope.h", + "typed_data/dart_byte_data.cc", + "typed_data/dart_byte_data.h", - public_deps = [ - "common", - "converter", - "file_loader", - "logging", - "scopes", - "typed_data", - "//third_party/dart/runtime:dart_api", + # Deprecated. + "filesystem/filesystem/eintr_wrapper.h", + "filesystem/filesystem/file.cc", + "filesystem/filesystem/file.h", + "filesystem/filesystem/path.h", + "filesystem/filesystem/portable_unistd.h", + "parsers/packages_map.cc", + "parsers/packages_map.h", + "typed_data/float32_list.h", + "typed_data/float64_list.h", + "typed_data/int32_list.h", + "typed_data/typed_list.cc", + "typed_data/typed_list.h", + "typed_data/uint16_list.h", + "typed_data/uint8_list.h", ] + if (is_win) { + sources += [ + "file_loader/file_loader_win.cc", + "filesystem/filesystem/path_win.cc", + ] + } else if (is_fuchsia) { + sources += [ + "file_loader/file_loader_fuchsia.cc", + "filesystem/filesystem/path_posix.cc", + ] + } else { + sources += [ + "file_loader/file_loader_posix.cc", + "filesystem/filesystem/path_posix.cc", + ] + } + + public_deps = [ "//third_party/dart/runtime:dart_api" ] + public_configs = [ ":config" ] } diff --git a/third_party/tonic/common/BUILD.gn b/third_party/tonic/common/BUILD.gn deleted file mode 100644 index 2af823348c554..0000000000000 --- a/third_party/tonic/common/BUILD.gn +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright 2013 The Flutter Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -source_set("common") { - visibility = [ "../*" ] - - public_configs = [ "../:config" ] - - sources = [ - "build_config.h", - "log.cc", - "log.h", - "macros.h", - ] -} diff --git a/third_party/tonic/converter/BUILD.gn b/third_party/tonic/converter/BUILD.gn deleted file mode 100644 index 1cde8c17e5ff4..0000000000000 --- a/third_party/tonic/converter/BUILD.gn +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright 2013 The Flutter Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -source_set("converter") { - visibility = [ "../*" ] - - configs += [ "../:config" ] - - sources = [ - "dart_converter.cc", - "dart_converter.h", - ] - - deps = [ "../common" ] - - public_deps = [ "//third_party/dart/runtime:dart_api" ] -} diff --git a/third_party/tonic/converter/dart_converter.h b/third_party/tonic/converter/dart_converter.h index 6693615a2d98d..2ba75a9c96aa9 100644 --- a/third_party/tonic/converter/dart_converter.h +++ b/third_party/tonic/converter/dart_converter.h @@ -6,10 +6,12 @@ #define LIB_CONVERTER_TONIC_DART_CONVERTER_H_ #include +#include #include #include "third_party/dart/runtime/include/dart_api.h" #include "tonic/common/macros.h" +#include "tonic/logging/dart_error.h" namespace tonic { @@ -279,15 +281,80 @@ struct DartConverter { //////////////////////////////////////////////////////////////////////////////// // Collections +inline Dart_Handle LookupNonNullableType(const std::string& library_name, + const std::string& type_name) { + auto library = + Dart_LookupLibrary(DartConverter::ToDart(library_name)); + if (LogIfError(library)) { + return library; + } + auto type_string = DartConverter::ToDart(type_name); + if (LogIfError(type_string)) { + return type_string; + } + auto type = Dart_GetNonNullableType(library, type_string, 0, nullptr); + if (LogIfError(type)) { + return type; + } + return type; +} + +template ::value, int> = 0> +Dart_Handle ToDartTypeHandle() { + return LookupNonNullableType("dart:core", "String"); +} + +template ::value, int> = 0> +Dart_Handle ToDartTypeHandle() { + return LookupNonNullableType("dart:core", "int"); +} + +template ::value, int> = 0> +Dart_Handle ToDartTypeHandle() { + return LookupNonNullableType("dart:core", "double"); +} + +template +Dart_Handle CreateZeroInitializedDartObject( + Dart_Handle type_handle_or_null = ::Dart_Null()) { + if constexpr (std::is_same::value) { + return ::Dart_EmptyString(); + } else if constexpr (std::is_integral::value) { + return ::Dart_NewIntegerFromUint64(0u); + } else if constexpr (std::is_floating_point::value) { + return ::Dart_NewDouble(0.0); + } else { + auto object = ::Dart_New(type_handle_or_null, ::Dart_Null(), 0, nullptr); + LogIfError(object); + return object; + } + return ::Dart_Null(); +} + template struct DartListFactory { - static Dart_Handle NewList(intptr_t length) { return Dart_NewList(length); } -}; - -template <> -struct DartListFactory { - static Dart_Handle NewList(intptr_t length) { - return Dart_NewListOf(Dart_CoreType_String, length); + static Dart_Handle NewList(Dart_Handle type_handle, intptr_t length) { + bool is_nullable = false; + auto is_nullable_handle = ::Dart_IsNullableType(type_handle, &is_nullable); + if (LogIfError(is_nullable_handle)) { + return is_nullable_handle; + } + if (is_nullable) { + auto list = ::Dart_NewListOfType(type_handle, length); + LogIfError(list); + return list; + } else { + auto sentinel = CreateZeroInitializedDartObject(type_handle); + if (LogIfError(sentinel)) { + return sentinel; + } + auto list = ::Dart_NewListOfTypeFilled(type_handle, sentinel, length); + LogIfError(list); + return list; + } + return ::Dart_Null(); } }; @@ -297,7 +364,8 @@ struct DartConverter> { using ConverterType = typename DartConverterTypes::ConverterType; static Dart_Handle ToDart(const std::vector& val) { - Dart_Handle list = DartListFactory::NewList(val.size()); + Dart_Handle list = DartListFactory::NewList( + ToDartTypeHandle(), val.size()); if (Dart_IsError(list)) return list; for (size_t i = 0; i < val.size(); i++) { diff --git a/third_party/tonic/dart_class_provider.cc b/third_party/tonic/dart_class_provider.cc index 2d9ca00642fbd..590b0b959a224 100644 --- a/third_party/tonic/dart_class_provider.cc +++ b/third_party/tonic/dart_class_provider.cc @@ -20,7 +20,7 @@ DartClassProvider::~DartClassProvider() {} Dart_Handle DartClassProvider::GetClassByName(const char* class_name) { Dart_Handle name_handle = ToDart(class_name); Dart_Handle class_handle = - Dart_GetType(library_.value(), name_handle, 0, nullptr); + Dart_GetNonNullableType(library_.value(), name_handle, 0, nullptr); TONIC_DCHECK(!Dart_IsError(class_handle)); return class_handle; } diff --git a/third_party/tonic/dart_wrappable.h b/third_party/tonic/dart_wrappable.h index 49b0a2c40baf3..a036abb854e7c 100644 --- a/third_party/tonic/dart_wrappable.h +++ b/third_party/tonic/dart_wrappable.h @@ -166,18 +166,6 @@ struct DartConverter> { } }; -template