Initialize Flutter Beta (flutter-3.32-candidate.0) (#166783)

Closes https://github.com/flutter/flutter/issues/166811.

```sh
$ dev/conductor/bin/conductor start \
  --candidate-branch=flutter-3.32-candidate.0 \
  --release-channel=beta \
  --github-username=matanlurey \
  --dart-revision=0d6811928830b87e36a0f49eb7fe554c308d3699 
```
This commit is contained in:
Matan Lurey 2025-04-09 15:57:49 -07:00 committed by GitHub
parent e2dea95082
commit 72ee26e314
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
62 changed files with 1258 additions and 631 deletions

View File

@ -5132,6 +5132,7 @@ targets:
- name: Mac_ios microbenchmarks_ios
recipe: devicelab/devicelab_drone
presubmit: false
bringup: true
timeout: 60
properties:
tags: >

16
DEPS
View File

@ -14,7 +14,7 @@ vars = {
'flutter_git': 'https://flutter.googlesource.com',
'skia_git': 'https://skia.googlesource.com',
'llvm_git': 'https://llvm.googlesource.com',
'skia_revision': '7b929584566c2c20f59d692301a40e55528a83a7',
'skia_revision': 'ac01f9306a0c08acf128d37bbd7b3e199525cc40',
# WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY
# See `lib/web_ui/README.md` for how to roll CanvasKit to a new version.
@ -56,16 +56,16 @@ vars = {
# Dart is: https://github.com/dart-lang/sdk/blob/main/DEPS
# You can use //tools/dart/create_updated_flutter_deps.py to produce
# updated revision list of existing dependencies.
'dart_revision': '87965ab4864e444c521023820eb06e569d007059',
'dart_revision': '0d6811928830b87e36a0f49eb7fe554c308d3699',
# 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_binaryen_rev': 'b4bdcc33115b31758c56b83bb9de4642c411a042',
'dart_boringssl_rev': 'e68438b863afaa3e81e1771b91819817780f3b60',
'dart_boringssl_rev': '8d8df26fc54f6e5fb0bf404a76ce973c0413eafc',
'dart_core_rev': '7a80178ca72b01b0efb99a9a9a654d83ca21d6b7',
'dart_devtools_rev': 'f10e8df8c517fb0412b9a66c626581867c9c267d',
'dart_devtools_rev': '1fb2f4ce5099042b7f2dfa93dec675a21861d21f',
'dart_ecosystem_rev': '391a80ccb774cfebe4865bcd7e933d1ab016eea5',
'dart_http_rev': '32d5ffcc8d0d5ce9a3a76a293230fa70ec2cc88f',
'dart_http_rev': '6fabf06b90d962cf9a6c009bbe919902ff1a1471',
'dart_i18n_rev': 'de1943629469719bf34269bf90fcdbe9334a73f3',
'dart_libprotobuf_rev': '24487dd1045c7f3d64a21f38a3f0c06cc4cf2edb',
'dart_perfetto_rev': '13ce0c9e13b0940d2476cd0cff2301708a9a2e2b',
@ -73,7 +73,7 @@ vars = {
'dart_protobuf_rev': '1aaa332af75c61ff32739821f7ec52186ff18d4c',
'dart_pub_rev': 'b2c03b448a47fdd52800609b9222cd737be3a934',
'dart_sync_http_rev': 'dc54465f07d9652875deeade643256dafa2fbc6c',
'dart_tools_rev': '62bc13bc086a66ce9a6a3e64865c82d17a1379b3',
'dart_tools_rev': '8d49319b95912a5dff3d7ba4cd861458c55c5608',
'dart_vector_math_rev': 'f08d7d2652e9ecf7d8f8605d9983335174511c95',
'dart_web_rev': '5a39fdc396ae40344308975140343c23b6863261',
'dart_webdev_rev': '697f2f7f56517b0678c6256e0834778905acfc0d',
@ -308,7 +308,7 @@ deps = {
Var('chromium_git') + '/external/github.com/WebAssembly/binaryen.git@b4bdcc33115b31758c56b83bb9de4642c411a042',
'engine/src/flutter/third_party/dart/third_party/devtools':
{'dep_type': 'cipd', 'packages': [{'package': 'dart/third_party/flutter/devtools', 'version': 'git_revision:f10e8df8c517fb0412b9a66c626581867c9c267d'}]},
{'dep_type': 'cipd', 'packages': [{'package': 'dart/third_party/flutter/devtools', 'version': 'git_revision:1fb2f4ce5099042b7f2dfa93dec675a21861d21f'}]},
'engine/src/flutter/third_party/dart/third_party/pkg/core':
Var('dart_git') + '/core.git' + '@' + Var('dart_core_rev'),
@ -332,7 +332,7 @@ deps = {
Var('dart_git') + '/leak_tracker.git@f5620600a5ce1c44f65ddaa02001e200b096e14c',
'engine/src/flutter/third_party/dart/third_party/pkg/native':
Var('dart_git') + '/native.git@75f3408cd72c1e217f162fb5e43f4cdf1d34e71d',
Var('dart_git') + '/native.git@4928765d7681ad8a4211c5977afd2f93f50b3f65',
'engine/src/flutter/third_party/dart/third_party/pkg/protobuf':
Var('dart_git') + '/protobuf.git' + '@' + Var('dart_protobuf_rev'),

View File

@ -1 +1 @@
267ac7b66308ad34b6ce14c1f5399ab0691f9ede
2405f6a2b7e1664e2779030c6b651676ecbe7651

View File

@ -0,0 +1 @@
flutter-3.32-candidate.0

View File

@ -22,7 +22,7 @@ dependencies:
characters: 1.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
clock: 1.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
csslib: 1.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
html: 0.15.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
html: 0.15.5+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
material_color_utilities: 0.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
meta: 1.16.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
path: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
@ -272,4 +272,4 @@ flutter:
- asset: packages/flutter_gallery_assets/fonts/merriweather/Merriweather-Regular.ttf
- asset: packages/flutter_gallery_assets/fonts/merriweather/Merriweather-Light.ttf
# PUBSPEC CHECKSUM: deed
# PUBSPEC CHECKSUM: 054a

View File

@ -26,7 +26,7 @@ dependencies:
dev_dependencies:
ffi: 2.1.4
ffigen: 18.0.0
ffigen: 18.1.0
flutter_lints: 5.0.0
test: 1.25.15
@ -68,4 +68,4 @@ dev_dependencies:
webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
yaml_edit: 2.2.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
# PUBSPEC CHECKSUM: a861
# PUBSPEC CHECKSUM: bf62

View File

@ -2,7 +2,7 @@ In the interest of transparency, we want to share high-level details of our road
Our plans will evolve over time based on customer feedback and new market opportunities. We will use our surveys and feedback on GitHub issues to prioritize work. The list here shouldn't be viewed either as exhaustive nor a promise that we will complete all this work. If you have feedback about what you think we should work on, we encourage you to get in touch by [filing an issue](https://github.com/flutter/flutter/issues/new/choose), or using the "thumbs-up" emoji reaction on an issue's first comment. Because Flutter is an open source project, we invite contributions both towards the themes presented below and in other areas.
_If you are a contributor or team of contributors with long-term plans for [contributing to Flutter](../../CONTRIBUTING.md), and would like your planned efforts reflected in the roadmap, please reach out to Hixie (ian@hixie.ch)._
_If you are a contributor or team of contributors with long-term plans for [contributing to Flutter](../../CONTRIBUTING.md), and would like your planned efforts reflected in the roadmap, please reach via email to `roadmap-input@flutter.dev`._
# 2025

View File

@ -1,4 +1,4 @@
Signature: 7582ce3fcdf2e515db0132d7c32bf1b3
Signature: c96f6439e1d3cadfbb6adc790efdcc37
====================================================================================================
LIBRARY: dart
@ -4862,7 +4862,7 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
You may obtain a copy of this library's Source Code Form from: https://dart.googlesource.com/sdk/+/4293d50dd30d5469fac05af7470148977327bbe8
You may obtain a copy of this library's Source Code Form from: https://dart.googlesource.com/sdk/+/0d6811928830b87e36a0f49eb7fe554c308d3699
/third_party/fallback_root_certificates/
====================================================================================================

View File

@ -1,4 +1,4 @@
Signature: cc16851918508797c4c52f81aeae260a
Signature: 6385fd7cd2f7be2e7ceee9d4b05eaf04
====================================================================================================
LIBRARY: etc1

View File

@ -938,6 +938,7 @@ TEST_P(AiksTest, ImageColorSourceEffectTransform) {
// Scale
{
builder.Save();
builder.Translate(100, 0);
builder.Scale(100, 100);
DlPaint paint;
@ -948,6 +949,23 @@ TEST_P(AiksTest, ImageColorSourceEffectTransform) {
DlImageSampling::kNearestNeighbor, &matrix));
builder.DrawRect(DlRect::MakeLTRB(0, 0, 1, 1), paint);
builder.Restore();
}
// Perspective
{
builder.Save();
builder.Translate(150, 150);
DlPaint paint;
DlMatrix matrix =
DlMatrix::MakePerspective(Radians{0.5}, ISize{200, 200}, 0.05, 1);
paint.setColorSource(DlColorSource::MakeImage(
texture, DlTileMode::kRepeat, DlTileMode::kRepeat,
DlImageSampling::kNearestNeighbor, &matrix));
builder.DrawRect(DlRect::MakeLTRB(0, 0, 200, 200), paint);
builder.Restore();
}
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));

View File

@ -1253,57 +1253,6 @@ TEST_P(AiksTest, BlurredRectangleWithShader) {
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}
TEST_P(AiksTest, GaussianBlurWithoutDecalSupport) {
if (GetParam() != PlaygroundBackend::kMetal) {
GTEST_SKIP()
<< "This backend doesn't yet support setting device capabilities.";
}
if (!WillRenderSomething()) {
// Sometimes these tests are run without playgrounds enabled which is
// pointless for this test since we are asserting that
// `SupportsDecalSamplerAddressMode` is called.
GTEST_SKIP() << "This test requires playgrounds.";
}
std::shared_ptr<const Capabilities> old_capabilities =
GetContext()->GetCapabilities();
auto mock_capabilities = std::make_shared<MockCapabilities>();
EXPECT_CALL(*mock_capabilities, SupportsDecalSamplerAddressMode())
.Times(::testing::AtLeast(1))
.WillRepeatedly(::testing::Return(false));
FLT_FORWARD(mock_capabilities, old_capabilities, GetDefaultColorFormat);
FLT_FORWARD(mock_capabilities, old_capabilities, GetDefaultStencilFormat);
FLT_FORWARD(mock_capabilities, old_capabilities,
GetDefaultDepthStencilFormat);
FLT_FORWARD(mock_capabilities, old_capabilities, SupportsOffscreenMSAA);
FLT_FORWARD(mock_capabilities, old_capabilities,
SupportsImplicitResolvingMSAA);
FLT_FORWARD(mock_capabilities, old_capabilities, SupportsReadFromResolve);
FLT_FORWARD(mock_capabilities, old_capabilities, SupportsFramebufferFetch);
FLT_FORWARD(mock_capabilities, old_capabilities, SupportsSSBO);
FLT_FORWARD(mock_capabilities, old_capabilities, SupportsCompute);
FLT_FORWARD(mock_capabilities, old_capabilities,
SupportsTextureToTextureBlits);
FLT_FORWARD(mock_capabilities, old_capabilities, GetDefaultGlyphAtlasFormat);
FLT_FORWARD(mock_capabilities, old_capabilities, SupportsTriangleFan);
FLT_FORWARD(mock_capabilities, old_capabilities, SupportsPrimitiveRestart);
ASSERT_TRUE(SetCapabilities(mock_capabilities).ok());
auto texture = DlImageImpeller::Make(CreateTextureForFixture("boston.jpg"));
DisplayListBuilder builder;
builder.Scale(GetContentScale().x * 0.5, GetContentScale().y * 0.5);
DlPaint paint;
paint.setColor(DlColor::kBlack());
builder.DrawPaint(paint);
auto blur_filter = DlImageFilter::MakeBlur(20, 20, DlTileMode::kDecal);
paint.setImageFilter(blur_filter);
builder.DrawImage(texture, DlPoint(200, 200), {}, &paint);
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}
// This addresses a bug where tiny blurs could result in mip maps that beyond
// the limits for the textures used for blurring.
// See also: b/323402168

View File

@ -197,7 +197,8 @@ std::shared_ptr<ColorSourceContents> Paint::CreateContents() const {
image_color_source->vertical_tile_mode());
auto sampler_descriptor =
skia_conversions::ToSamplerDescriptor(image_color_source->sampling());
auto effect_transform = image_color_source->matrix();
// See https://github.com/flutter/flutter/issues/165205
flutter::DlMatrix effect_transform = image_color_source->matrix().To3x3();
auto contents = std::make_shared<TiledTextureContents>();
contents->SetOpacityFactor(color.alpha);

View File

@ -247,6 +247,19 @@ struct Matrix {
// clang-format on
}
// Converts the second row/col to identity to make this an equivalent
// to a Skia 3x3 Matrix.
constexpr Matrix To3x3() const {
// clang-format off
return Matrix(
m[0], m[1], 0, m[3],
m[4], m[5], 0, m[7],
0, 0, 1, 0,
m[12], m[13], 0, m[15]
);
// clang-format on
}
constexpr Matrix Translate(const Vector3& t) const {
// clang-format off
return Matrix(m[0], m[1], m[2], m[3],

View File

@ -313,5 +313,14 @@ TEST(MatrixTest, MakeScaleTranslate) {
Matrix::MakeTranslation({0, 0, 0}) * Matrix::MakeScale({0, 0, 0})));
}
TEST(MatrixTest, To3x3) {
Matrix x(1.0, 0.0, 4.0, 0.0, //
0.0, 1.0, 4.0, 0.0, //
6.0, 5.0, 111.0, 7.0, //
0.0, 0.0, 9.0, 1.0);
EXPECT_TRUE(MatrixNear(x.To3x3(), Matrix()));
}
} // namespace testing
} // namespace impeller

View File

@ -5,6 +5,7 @@
#include "impeller/renderer/backend/vulkan/allocator_vk.h"
#include <memory>
#include <utility>
#include "flutter/fml/memory/ref_ptr.h"
#include "flutter/fml/trace_event.h"
@ -12,6 +13,7 @@
#include "impeller/core/formats.h"
#include "impeller/renderer/backend/vulkan/capabilities_vk.h"
#include "impeller/renderer/backend/vulkan/device_buffer_vk.h"
#include "impeller/renderer/backend/vulkan/device_holder_vk.h"
#include "impeller/renderer/backend/vulkan/formats_vk.h"
#include "impeller/renderer/backend/vulkan/texture_vk.h"
#include "vulkan/vulkan_enums.hpp"
@ -405,9 +407,10 @@ class AllocatedTextureSourceVK final : public TextureSourceVK {
return;
}
resource_.Swap(ImageResource(ImageVMA{allocator, allocation, image},
std::move(image_view),
std::move(rt_image_view)));
resource_.Swap(ImageResource(
ImageVMA{allocator, allocation, image}, std::move(image_view),
std::move(rt_image_view), context.GetResourceAllocator(),
context.GetDeviceHolder()));
is_valid_ = true;
}
@ -429,6 +432,8 @@ class AllocatedTextureSourceVK final : public TextureSourceVK {
private:
struct ImageResource {
std::shared_ptr<DeviceHolderVK> device_holder;
std::shared_ptr<Allocator> allocator;
UniqueImageVMA image;
vk::UniqueImageView image_view;
vk::UniqueImageView rt_image_view;
@ -437,8 +442,12 @@ class AllocatedTextureSourceVK final : public TextureSourceVK {
ImageResource(ImageVMA p_image,
vk::UniqueImageView p_image_view,
vk::UniqueImageView p_rt_image_view)
: image(p_image),
vk::UniqueImageView p_rt_image_view,
std::shared_ptr<Allocator> allocator,
std::shared_ptr<DeviceHolderVK> device_holder)
: device_holder(std::move(device_holder)),
allocator(std::move(allocator)),
image(p_image),
image_view(std::move(p_image_view)),
rt_image_view(std::move(p_rt_image_view)) {}

View File

@ -5,6 +5,7 @@
#include "flutter/testing/testing.h" // IWYU pragma: keep
#include "gtest/gtest.h"
#include "impeller/base/allocation_size.h"
#include "impeller/core/allocator.h"
#include "impeller/core/device_buffer.h"
#include "impeller/core/device_buffer_descriptor.h"
#include "impeller/core/formats.h"
@ -73,6 +74,25 @@ TEST(AllocatorVKTest, MemoryTypeSelectionTwoHeap) {
EXPECT_EQ(AllocatorVK::FindMemoryTypeIndex(4, properties), -1);
}
TEST(AllocatorVKTest, ImageResourceKeepsVulkanDeviceAlive) {
std::shared_ptr<Texture> texture;
std::weak_ptr<Allocator> weak_allocator;
{
auto const context = MockVulkanContextBuilder().Build();
weak_allocator = context->GetResourceAllocator();
auto allocator = context->GetResourceAllocator();
texture = allocator->CreateTexture(TextureDescriptor{
.storage_mode = StorageMode::kDevicePrivate,
.format = PixelFormat::kR8G8B8A8UNormInt,
.size = {1, 1},
});
context->Shutdown();
}
ASSERT_TRUE(weak_allocator.lock());
}
#ifdef IMPELLER_DEBUG
TEST(AllocatorVKTest, RecreateSwapchainWhenSizeChanges) {

View File

@ -12,7 +12,6 @@
#include "impeller/renderer/backend/vulkan/shared_object_vk.h"
#include "impeller/renderer/backend/vulkan/vk.h"
#include "impeller/renderer/backend/vulkan/yuv_conversion_vk.h"
#include "vulkan/vulkan_handles.hpp"
namespace impeller {

View File

@ -1,6 +1,7 @@
// 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:typed_data';
import 'package:meta/meta.dart';
import 'package:ui/src/engine.dart';
@ -9,20 +10,10 @@ import 'package:ui/ui.dart' as ui;
/// Implements vertical and horizontal scrolling functionality for semantics
/// objects.
///
/// Scrolling is implemented using a "joystick" method. The absolute value of
/// "scrollTop" in HTML is not important. We only need to know in whether the
/// value changed in the positive or negative direction. If it changes in the
/// positive direction we send a [ui.SemanticsAction.scrollUp]. Otherwise, we
/// send [ui.SemanticsAction.scrollDown]. The actual scrolling is then handled
/// by the framework and we receive a [ui.SemanticsUpdate] containing the new
/// [scrollPosition] and child positions.
///
/// "scrollTop" or "scrollLeft" is always reset to an arbitrarily chosen non-
/// zero "neutral" scroll position value. This is done so we have a
/// predictable range of DOM scroll position values. When the amount of
/// contents is less than the size of the viewport the browser snaps
/// "scrollTop" back to zero. If there is more content than available in the
/// viewport "scrollTop" may take positive values.
/// Scrolling is controlled by sending the current DOM scroll position in a
/// [ui.SemanticsAction.scrollToOffset] to the framework where it applies the
/// value to its scrollable and the engine receives a [ui.SemanticsUpdate]
/// containing the new [SemanticsObject.scrollPosition] and child positions.
class SemanticScrollable extends SemanticRole {
SemanticScrollable(SemanticsObject semanticsObject)
: super.withBasics(
@ -39,81 +30,61 @@ class SemanticScrollable extends SemanticRole {
/// Disables browser-driven scrolling in the presence of pointer events.
GestureModeCallback? _gestureModeListener;
/// DOM element used as a workaround for: https://github.com/flutter/flutter/issues/104036
///
/// When the assistive technology gets to the last element of the scrollable
/// list, the browser thinks the scrollable area doesn't have any more content,
/// so it overrides the value of "scrollTop"/"scrollLeft" with zero. As a result,
/// the user can't scroll back up/left.
///
/// As a workaround, we add this DOM element and set its size to
/// [canonicalNeutralScrollPosition] so the browser believes
/// that the scrollable area still has some more content, and doesn't override
/// scrollTop/scrollLetf with zero.
/// DOM element used to indicate to the browser the total quantity of available
/// content under this scrollable area. This element is sized based on the
/// total scroll extent calculated by scrollExtentMax - scrollExtentMin + rect.height
/// of the [SemanticsObject] managed by this scrollable.
final DomElement _scrollOverflowElement = createDomElement('flt-semantics-scroll-overflow');
/// Listens to HTML "scroll" gestures detected by the browser.
///
/// This gesture is converted to [ui.SemanticsAction.scrollUp] or
/// [ui.SemanticsAction.scrollDown], depending on the direction.
/// When the browser detects a "scroll" gesture we send the updated DOM scroll position
/// to the framework in a [ui.SemanticsAction.scrollToOffset].
@visibleForTesting
DomEventListener? scrollListener;
/// The value of the "scrollTop" or "scrollLeft" property of this object's
/// [element] that has zero offset relative to the [scrollPosition].
int _effectiveNeutralScrollPosition = 0;
/// Whether this scrollable can scroll vertically or horizontally.
bool get _canScroll =>
semanticsObject.isVerticalScrollContainer || semanticsObject.isHorizontalScrollContainer;
/// The previous value of the "scrollTop" or "scrollLeft" property of this object's
/// [element], used to determine if the content was scrolled.
int _previousDomScrollPosition = 0;
/// Responds to browser-detected "scroll" gestures.
void _recomputeScrollPosition() {
if (_domScrollPosition != _effectiveNeutralScrollPosition) {
if (_domScrollPosition != _previousDomScrollPosition) {
if (!EngineSemantics.instance.shouldAcceptBrowserGesture('scroll')) {
return;
}
final bool doScrollForward = _domScrollPosition > _effectiveNeutralScrollPosition;
_neutralizeDomScrollPosition();
_previousDomScrollPosition = _domScrollPosition;
_updateScrollableState();
semanticsObject.recomputePositionAndSize();
semanticsObject.updateChildrenPositionAndSize();
final int semanticsId = semanticsObject.id;
if (doScrollForward) {
if (semanticsObject.isVerticalScrollContainer) {
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
viewId,
semanticsId,
ui.SemanticsAction.scrollUp,
null,
);
} else {
assert(semanticsObject.isHorizontalScrollContainer);
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
viewId,
semanticsId,
ui.SemanticsAction.scrollLeft,
null,
);
}
final Float64List offsets = Float64List(2);
// Either SemanticsObject.isVerticalScrollContainer or
// SemanticsObject.isHorizontalScrollContainer should be
// true otherwise scrollToOffset cannot be called.
if (semanticsObject.isVerticalScrollContainer) {
offsets[0] = 0.0;
offsets[1] = element.scrollTop;
} else {
if (semanticsObject.isVerticalScrollContainer) {
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
viewId,
semanticsId,
ui.SemanticsAction.scrollDown,
null,
);
} else {
assert(semanticsObject.isHorizontalScrollContainer);
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
viewId,
semanticsId,
ui.SemanticsAction.scrollRight,
null,
);
}
assert(semanticsObject.isHorizontalScrollContainer);
offsets[0] = element.scrollLeft;
offsets[1] = 0.0;
}
final ByteData? message = const StandardMessageCodec().encodeMessage(offsets);
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
viewId,
semanticsId,
ui.SemanticsAction.scrollToOffset,
message,
);
}
}
@ -122,6 +93,22 @@ class SemanticScrollable extends SemanticRole {
// Scrolling is controlled by setting overflow-y/overflow-x to 'scroll`. The
// default overflow = "visible" needs to be unset.
semanticsObject.element.style.overflow = '';
// On macOS the scrollbar behavior which can be set in the settings application
// may sometimes insert scrollbars into an application when a peripheral like a
// mouse or keyboard is plugged in. This causes the clientHeight or clientWidth
// of the scrollable DOM element to be offset by the width of the scrollbar.
// This causes issues in the vertical scrolling context because the max scroll
// extent is calculated by the element's scrollHeight - clientHeight, so when
// the clientHeight is offset by scrollbar width the browser may there is
// a greater scroll extent then what is actually available.
//
// The scrollbar is already made transparent in SemanticsRole._initElement so here
// set scrollbar-width to "none" to prevent it from affecting the max scroll extent.
//
// Support for scrollbar-width was only added to Safari v18.2+, so versions before
// that may still experience overscroll issues when macOS inserts scrollbars
// into the application.
semanticsObject.element.style.scrollbarWidth = 'none';
_scrollOverflowElement.style
..position = 'absolute'
@ -136,7 +123,15 @@ class SemanticScrollable extends SemanticRole {
super.update();
semanticsObject.owner.addOneTimePostUpdateCallback(() {
_neutralizeDomScrollPosition();
if (_canScroll) {
final double? scrollPosition = semanticsObject.scrollPosition;
assert(scrollPosition != null);
if (scrollPosition != _domScrollPosition) {
element.scrollTop = scrollPosition!;
_previousDomScrollPosition = _domScrollPosition;
}
}
_updateScrollableState();
semanticsObject.recomputePositionAndSize();
semanticsObject.updateChildrenPositionAndSize();
});
@ -183,56 +178,38 @@ class SemanticScrollable extends SemanticRole {
}
}
/// Resets the scroll position (top or left) to the neutral value.
///
/// The scroll position of the scrollable HTML node that's considered to
/// have zero offset relative to Flutter's notion of scroll position is
/// referred to as "neutral scroll position".
///
/// We always set the scroll position to a non-zero value in order to
/// be able to scroll in the negative direction. When scrollTop/scrollLeft is
/// zero the browser will refuse to scroll back even when there is more
/// content available.
void _neutralizeDomScrollPosition() {
void _updateScrollableState() {
// This value is arbitrary.
const int canonicalNeutralScrollPosition = 10;
final ui.Rect? rect = semanticsObject.rect;
if (rect == null) {
printWarning('Warning! the rect attribute of semanticsObject is null');
return;
}
final double? scrollExtentMax = semanticsObject.scrollExtentMax;
final double? scrollExtentMin = semanticsObject.scrollExtentMin;
assert(scrollExtentMax != null);
assert(scrollExtentMin != null);
final double scrollExtentTotal =
scrollExtentMax! -
scrollExtentMin! +
(semanticsObject.isVerticalScrollContainer ? rect.height : rect.width);
// Place the _scrollOverflowElement at the beginning of the content
// and size it based on the total scroll extent so the browser
// knows how much scrollable content there is.
if (semanticsObject.isVerticalScrollContainer) {
// Place the _scrollOverflowElement at the end of the content and
// make sure that when we neutralize the scrolling position,
// it doesn't scroll into the visible area.
final int verticalOffset = rect.height.ceil() + canonicalNeutralScrollPosition;
_scrollOverflowElement.style
..transform = 'translate(0px,${verticalOffset}px)'
..width = '${rect.width.round()}px'
..height = '${canonicalNeutralScrollPosition}px';
element.scrollTop = canonicalNeutralScrollPosition.toDouble();
// Read back because the effective value depends on the amount of content.
_effectiveNeutralScrollPosition = element.scrollTop.toInt();
..width = '0px'
..height = '${scrollExtentTotal.toStringAsFixed(1)}px';
semanticsObject
..verticalScrollAdjustment = _effectiveNeutralScrollPosition.toDouble()
..verticalScrollAdjustment = element.scrollTop
..horizontalScrollAdjustment = 0.0;
} else if (semanticsObject.isHorizontalScrollContainer) {
// Place the _scrollOverflowElement at the end of the content and
// make sure that when we neutralize the scrolling position,
// it doesn't scroll into the visible area.
final int horizontalOffset = rect.width.ceil() + canonicalNeutralScrollPosition;
_scrollOverflowElement.style
..transform = 'translate(${horizontalOffset}px,0px)'
..width = '${canonicalNeutralScrollPosition}px'
..height = '${rect.height.round()}px';
element.scrollLeft = canonicalNeutralScrollPosition.toDouble();
// Read back because the effective value depends on the amount of content.
_effectiveNeutralScrollPosition = element.scrollLeft.toInt();
..width = '${scrollExtentTotal.toStringAsFixed(1)}px'
..height = '0px';
semanticsObject
..verticalScrollAdjustment = 0.0
..horizontalScrollAdjustment = _effectiveNeutralScrollPosition.toDouble();
..horizontalScrollAdjustment = element.scrollLeft;
} else {
_scrollOverflowElement.style
..transform = 'translate(0px,0px)'
@ -240,7 +217,6 @@ class SemanticScrollable extends SemanticRole {
..height = '0px';
element.scrollLeft = 0.0;
element.scrollTop = 0.0;
_effectiveNeutralScrollPosition = 0;
semanticsObject
..verticalScrollAdjustment = 0.0
..horizontalScrollAdjustment = 0.0;

View File

@ -1612,7 +1612,7 @@ void _testVerticalScrolling() {
</sem>''');
final DomElement scrollable = findScrollable(owner());
expect(scrollable.scrollTop, isPositive);
expect(scrollable.scrollTop, 0);
semantics().semanticsEnabled = false;
});
@ -1649,8 +1649,8 @@ void _testVerticalScrolling() {
expect(scrollable, isNotNull);
// When there's less content than the available size the neutral scrollTop
// is still a positive number.
expect(scrollable.scrollTop, isPositive);
// is 0.
expect(scrollable.scrollTop, 0);
semantics().semanticsEnabled = false;
});
@ -1703,18 +1703,7 @@ void _testVerticalScrolling() {
final DomElement scrollable = owner().debugSemanticsTree![0]!.element;
expect(scrollable, isNotNull);
// When there's more content than the available size the neutral scrollTop
// is greater than 0 with a maximum of 10 or 9.
int browserMaxScrollDiff = 0;
// The max scroll value varies between `9` and `10` for Safari desktop
// browsers.
if (ui_web.browser.browserEngine == ui_web.BrowserEngine.webkit &&
ui_web.browser.operatingSystem == ui_web.OperatingSystem.macOs) {
browserMaxScrollDiff = 1;
}
expect(scrollable.scrollTop >= (10 - browserMaxScrollDiff), isTrue);
expect(scrollable.scrollTop, 0);
Future<ui.SemanticsActionEvent> capturedEventFuture = captureSemanticsEvent();
scrollable.scrollTop = 20;
@ -1722,21 +1711,44 @@ void _testVerticalScrolling() {
ui.SemanticsActionEvent capturedEvent = await capturedEventFuture;
expect(capturedEvent.nodeId, 0);
expect(capturedEvent.type, ui.SemanticsAction.scrollUp);
expect(capturedEvent.arguments, isNull);
// Engine semantics returns scroll top back to neutral.
expect(scrollable.scrollTop >= (10 - browserMaxScrollDiff), isTrue);
expect(capturedEvent.type, ui.SemanticsAction.scrollToOffset);
expect(capturedEvent.arguments, isNotNull);
final Float64List expectedOffset = Float64List(2);
expectedOffset[0] = 0.0;
expectedOffset[1] = 20.0;
Float64List message =
const StandardMessageCodec().decodeMessage(capturedEvent.arguments! as ByteData)
as Float64List;
expect(message, expectedOffset);
// Update scrollPosition to scrollTop value.
final ui.SemanticsUpdateBuilder builder2 = ui.SemanticsUpdateBuilder();
updateNode(
builder2,
scrollPosition: 20.0,
flags: 0 | ui.SemanticsFlag.hasImplicitScrolling.index,
actions: 0 | ui.SemanticsAction.scrollUp.index | ui.SemanticsAction.scrollDown.index,
transform: Matrix4.identity().toFloat64(),
rect: const ui.Rect.fromLTRB(0, 0, 50, 100),
childrenInHitTestOrder: Int32List.fromList(<int>[1, 2, 3]),
childrenInTraversalOrder: Int32List.fromList(<int>[1, 2, 3]),
);
owner().updateSemantics(builder2.build());
capturedEventFuture = captureSemanticsEvent();
scrollable.scrollTop = 5;
capturedEvent = await capturedEventFuture;
expect(scrollable.scrollTop >= (5 - browserMaxScrollDiff), isTrue);
expect(scrollable.scrollTop, 5);
expect(capturedEvent.nodeId, 0);
expect(capturedEvent.type, ui.SemanticsAction.scrollDown);
expect(capturedEvent.arguments, isNull);
// Engine semantics returns scroll top back to neutral.
expect(scrollable.scrollTop >= (10 - browserMaxScrollDiff), isTrue);
expect(capturedEvent.type, ui.SemanticsAction.scrollToOffset);
expect(capturedEvent.arguments, isNotNull);
expectedOffset[0] = 0.0;
expectedOffset[1] = 5.0;
message =
const StandardMessageCodec().decodeMessage(capturedEvent.arguments! as ByteData)
as Float64List;
expect(message, expectedOffset);
});
test('scrollable switches to pointer event mode on a wheel event', () async {
@ -1783,27 +1795,22 @@ void _testVerticalScrolling() {
final DomElement scrollable = owner().debugSemanticsTree![0]!.element;
expect(scrollable, isNotNull);
void expectNeutralPosition() {
// Browsers disagree on the exact value, but it's always close to 10.
expect((scrollable.scrollTop - 10).abs(), lessThan(2));
}
// Initially, starting with a neutral scroll position, everything should be
// Initially, starting at "scrollTop" 0, everything should be
// in browser gesture mode, react to DOM scroll events, and generate
// semantic actions.
expectNeutralPosition();
expect(scrollable.scrollTop, 0);
expect(semantics().gestureMode, GestureMode.browserGestures);
scrollable.scrollTop = 20;
expect(scrollable.scrollTop, 20);
await Future<void>.delayed(const Duration(milliseconds: 100));
expect(actionLog, hasLength(1));
final capturedEvent = actionLog.removeLast();
expect(capturedEvent.type, ui.SemanticsAction.scrollUp);
expect(capturedEvent.type, ui.SemanticsAction.scrollToOffset);
// Now, starting with a neutral mode, observing a DOM "wheel" event should
// Now, starting at the "scrollTop" 20 we set, observing a DOM "wheel" event should
// swap into pointer event mode, and the scrollable becomes a plain clip,
// i.e. `overflow: hidden`.
expectNeutralPosition();
expect(scrollable.scrollTop, 20);
expect(semantics().gestureMode, GestureMode.browserGestures);
expect(scrollable.style.overflowY, 'scroll');
@ -1870,8 +1877,8 @@ void _testHorizontalScrolling() {
expect(scrollable, isNotNull);
// When there's less content than the available size the neutral
// scrollLeft is still a positive number.
expect(scrollable.scrollLeft, isPositive);
// scrollLeft is still 0.
expect(scrollable.scrollLeft, 0);
semantics().semanticsEnabled = false;
});
@ -1924,17 +1931,7 @@ void _testHorizontalScrolling() {
final DomElement scrollable = findScrollable(owner());
expect(scrollable, isNotNull);
// When there's more content than the available size the neutral scrollTop
// is greater than 0 with a maximum of 10.
int browserMaxScrollDiff = 0;
// The max scroll value varies between `9` and `10` for Safari desktop
// browsers.
if (ui_web.browser.browserEngine == ui_web.BrowserEngine.webkit &&
ui_web.browser.operatingSystem == ui_web.OperatingSystem.macOs) {
browserMaxScrollDiff = 1;
}
expect(scrollable.scrollLeft >= (10 - browserMaxScrollDiff), isTrue);
expect(scrollable.scrollLeft, 0);
Future<ui.SemanticsActionEvent> capturedEventFuture = captureSemanticsEvent();
scrollable.scrollLeft = 20;
@ -1942,21 +1939,44 @@ void _testHorizontalScrolling() {
ui.SemanticsActionEvent capturedEvent = await capturedEventFuture;
expect(capturedEvent.nodeId, 0);
expect(capturedEvent.type, ui.SemanticsAction.scrollLeft);
expect(capturedEvent.arguments, isNull);
// Engine semantics returns scroll position back to neutral.
expect(scrollable.scrollLeft >= (10 - browserMaxScrollDiff), isTrue);
expect(capturedEvent.type, ui.SemanticsAction.scrollToOffset);
expect(capturedEvent.arguments, isNotNull);
final Float64List expectedOffset = Float64List(2);
expectedOffset[0] = 20.0;
expectedOffset[1] = 0.0;
Float64List message =
const StandardMessageCodec().decodeMessage(capturedEvent.arguments! as ByteData)
as Float64List;
expect(message, expectedOffset);
// Update scrollPosition to scrollLeft value.
final ui.SemanticsUpdateBuilder builder2 = ui.SemanticsUpdateBuilder();
updateNode(
builder2,
scrollPosition: 20.0,
flags: 0 | ui.SemanticsFlag.hasImplicitScrolling.index,
actions: 0 | ui.SemanticsAction.scrollLeft.index | ui.SemanticsAction.scrollRight.index,
transform: Matrix4.identity().toFloat64(),
rect: const ui.Rect.fromLTRB(0, 0, 50, 100),
childrenInHitTestOrder: Int32List.fromList(<int>[1, 2, 3]),
childrenInTraversalOrder: Int32List.fromList(<int>[1, 2, 3]),
);
owner().updateSemantics(builder2.build());
capturedEventFuture = captureSemanticsEvent();
scrollable.scrollLeft = 5;
capturedEvent = await capturedEventFuture;
expect(scrollable.scrollLeft >= (5 - browserMaxScrollDiff), isTrue);
expect(scrollable.scrollLeft, 5);
expect(capturedEvent.nodeId, 0);
expect(capturedEvent.type, ui.SemanticsAction.scrollRight);
expect(capturedEvent.arguments, isNull);
// Engine semantics returns scroll top back to neutral.
expect(scrollable.scrollLeft >= (10 - browserMaxScrollDiff), isTrue);
expect(capturedEvent.type, ui.SemanticsAction.scrollToOffset);
expect(capturedEvent.arguments, isNotNull);
expectedOffset[0] = 5.0;
expectedOffset[1] = 0.0;
message =
const StandardMessageCodec().decodeMessage(capturedEvent.arguments! as ByteData)
as Float64List;
expect(message, expectedOffset);
});
}

View File

@ -166,8 +166,6 @@ dependency_overrides:
path: ./third_party/pkg/googleapis/discoveryapis_commons
_fe_analyzer_shared:
path: ./third_party/dart/pkg/_fe_analyzer_shared
_macros:
path: ./third_party/dart/pkg/_macros
analyzer:
path: ./third_party/dart/pkg/analyzer
archive:
@ -222,8 +220,6 @@ dependency_overrides:
path: ./third_party/dart/pkg/kernel
logging:
path: ./third_party/dart/third_party/pkg/core/pkgs/logging
macros:
path: ./third_party/dart/pkg/macros
matcher:
path: ./third_party/dart/third_party/pkg/test/pkgs/matcher
meta:

View File

@ -18,8 +18,6 @@ dev_dependencies:
dependency_overrides: # Must include all transitive dependencies from the "any" packages above.
_fe_analyzer_shared:
path: ../third_party/dart/pkg/_fe_analyzer_shared
_macros:
path: ../third_party/dart/pkg/_macros
analyzer:
path: ../third_party/dart/pkg/analyzer
args:
@ -38,8 +36,6 @@ dependency_overrides: # Must include all transitive dependencies from the "any"
path: ../third_party/dart/third_party/pkg/tools/pkgs/file
glob:
path: ../third_party/dart/third_party/pkg/tools/pkgs/glob
macros:
path: ../third_party/dart/pkg/macros
meta:
path: ../third_party/dart/pkg/meta
package_config:

View File

@ -0,0 +1,207 @@
// Copyright 2014 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 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
/// Flutter code sample for [SliverEnsureSemantics].
void main() => runApp(const SliverEnsureSemanticsExampleApp());
class SliverEnsureSemanticsExampleApp extends StatelessWidget {
const SliverEnsureSemanticsExampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: SliverEnsureSemanticsExample());
}
}
class SliverEnsureSemanticsExample extends StatefulWidget {
const SliverEnsureSemanticsExample({super.key});
@override
State<SliverEnsureSemanticsExample> createState() => _SliverEnsureSemanticsExampleState();
}
class _SliverEnsureSemanticsExampleState extends State<SliverEnsureSemanticsExample> {
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
backgroundColor: theme.colorScheme.inversePrimary,
title: const Text('SliverEnsureSemantics Demo'),
),
body: Center(
child: CustomScrollView(
semanticChildCount: 106,
slivers: <Widget>[
SliverEnsureSemantics(
sliver: SliverToBoxAdapter(
child: IndexedSemantics(
index: 0,
child: Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Semantics(
header: true,
headingLevel: 3,
child: Text('Steps to reproduce', style: theme.textTheme.headlineSmall),
),
const Text('Issue description'),
Semantics(
header: true,
headingLevel: 3,
child: Text('Expected Results', style: theme.textTheme.headlineSmall),
),
Semantics(
header: true,
headingLevel: 3,
child: Text('Actual Results', style: theme.textTheme.headlineSmall),
),
Semantics(
header: true,
headingLevel: 3,
child: Text('Code Sample', style: theme.textTheme.headlineSmall),
),
Semantics(
header: true,
headingLevel: 3,
child: Text('Screenshots', style: theme.textTheme.headlineSmall),
),
Semantics(
header: true,
headingLevel: 3,
child: Text('Logs', style: theme.textTheme.headlineSmall),
),
],
),
),
),
),
),
),
SliverFixedExtentList(
itemExtent: 44.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Card(
child: Padding(padding: const EdgeInsets.all(8.0), child: Text('Item $index')),
);
},
childCount: 50,
semanticIndexOffset: 1,
),
),
SliverEnsureSemantics(
sliver: SliverToBoxAdapter(
child: IndexedSemantics(
index: 51,
child: Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Semantics(header: true, child: const Text('Footer 1')),
),
),
),
),
),
SliverEnsureSemantics(
sliver: SliverToBoxAdapter(
child: IndexedSemantics(
index: 52,
child: Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Semantics(header: true, child: const Text('Footer 2')),
),
),
),
),
),
SliverEnsureSemantics(
sliver: SliverToBoxAdapter(
child: IndexedSemantics(
index: 53,
child: Semantics(link: true, child: const Text('Link #1')),
),
),
),
SliverEnsureSemantics(
sliver: SliverToBoxAdapter(
child: IndexedSemantics(
index: 54,
child: OverflowBar(
children: <Widget>[
TextButton(onPressed: () {}, child: const Text('Button 1')),
TextButton(onPressed: () {}, child: const Text('Button 2')),
],
),
),
),
),
SliverEnsureSemantics(
sliver: SliverToBoxAdapter(
child: IndexedSemantics(
index: 55,
child: Semantics(link: true, child: const Text('Link #2')),
),
),
),
SliverEnsureSemantics(
sliver: SliverSemanticsList(
sliver: SliverFixedExtentList(
itemExtent: 44.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Semantics(
role: SemanticsRole.listItem,
child: Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text('Second List Item $index'),
),
),
);
},
childCount: 50,
semanticIndexOffset: 56,
),
),
),
),
SliverEnsureSemantics(
sliver: SliverToBoxAdapter(
child: IndexedSemantics(
index: 107,
child: Semantics(link: true, child: const Text('Link #3')),
),
),
),
],
),
),
);
}
}
// A sliver that assigns the role of SemanticsRole.list to its sliver child.
class SliverSemanticsList extends SingleChildRenderObjectWidget {
const SliverSemanticsList({super.key, required Widget sliver}) : super(child: sliver);
@override
RenderSliverSemanticsList createRenderObject(BuildContext context) => RenderSliverSemanticsList();
}
class RenderSliverSemanticsList extends RenderProxySliver {
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.role = SemanticsRole.list;
}
}

View File

@ -0,0 +1,14 @@
// Copyright 2014 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 'package:flutter_api_samples/widgets/sliver/sliver_ensure_semantics.0.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('SliverEnsureSemantics example', (WidgetTester tester) async {
await tester.pumpWidget(const example.SliverEnsureSemanticsExampleApp());
expect(find.text('SliverEnsureSemantics Demo'), findsOneWidget);
});
}

View File

@ -107,6 +107,15 @@ import 'theme.dart';
/// Here is an example to show different carousel layouts that [CarouselView]
/// and [CarouselView.weighted] can build.
///
/// On desktop and web running on desktop platforms, dragging to scroll with a mouse
/// is disabled by default to align with natural behavior.
///
/// To further align expected behavior like this, mouse input can scroll horizontally
/// by pressing the shift key while scrolling with the mouse wheel.
///
/// This key-driven behavior is dictated by the [ScrollBehavior.pointerAxisModifiers],
/// while [ScrollBehavior.dragDevices] manages what devices can drag a scrollable.
///
/// ** See code in examples/api/lib/material/carousel/carousel.0.dart **
/// {@end-tool}
///

View File

@ -393,7 +393,7 @@ class ListTile extends StatelessWidget {
this.title,
this.subtitle,
this.trailing,
this.isThreeLine = false,
this.isThreeLine,
this.dense,
this.visualDensity,
this.shape,
@ -425,7 +425,7 @@ class ListTile extends StatelessWidget {
this.minTileHeight,
this.titleAlignment,
this.internalAddSemanticForOnTap = true,
}) : assert(!isThreeLine || subtitle != null);
}) : assert(isThreeLine != true || subtitle != null);
/// A widget to display before the title.
///
@ -482,7 +482,12 @@ class ListTile extends StatelessWidget {
///
/// When using a [Text] widget for [title] and [subtitle], you can enforce
/// line limits using [Text.maxLines].
final bool isThreeLine;
///
/// See also:
///
/// * [ListTileTheme.of], which returns the nearest [ListTileTheme]'s
/// [ListTileThemeData].
final bool? isThreeLine;
/// {@template flutter.material.ListTile.dense}
/// Whether this list tile is part of a vertically dense list.
@ -987,7 +992,11 @@ class ListTile extends StatelessWidget {
trailing: trailingIcon,
isDense: _isDenseLayout(theme, tileTheme),
visualDensity: visualDensity ?? tileTheme.visualDensity ?? theme.visualDensity,
isThreeLine: isThreeLine,
isThreeLine:
isThreeLine ??
tileTheme.isThreeLine ??
theme.listTileTheme.isThreeLine ??
false,
textDirection: textDirection,
titleBaselineType:
titleStyle.textBaseline ?? defaults.titleTextStyle!.textBaseline!,
@ -1021,7 +1030,6 @@ class ListTile extends StatelessWidget {
ifTrue: 'THREE_LINE',
ifFalse: 'TWO_LINE',
showName: true,
defaultValue: false,
),
);
properties.add(

View File

@ -73,6 +73,7 @@ class ListTileThemeData with Diagnosticable {
this.minTileHeight,
this.titleAlignment,
this.controlAffinity,
this.isThreeLine,
});
/// Overrides the default value of [ListTile.dense].
@ -139,6 +140,9 @@ class ListTileThemeData with Diagnosticable {
/// or [ExpansionTile.controlAffinity] or [SwitchListTile.controlAffinity] or [RadioListTile.controlAffinity].
final ListTileControlAffinity? controlAffinity;
/// If specified, overrides the default value of [ListTile.isThreeLine].
final bool? isThreeLine;
/// Creates a copy of this object with the given fields replaced with the
/// new values.
ListTileThemeData copyWith({
@ -187,6 +191,7 @@ class ListTileThemeData with Diagnosticable {
visualDensity: visualDensity ?? this.visualDensity,
titleAlignment: titleAlignment ?? this.titleAlignment,
controlAffinity: controlAffinity ?? this.controlAffinity,
isThreeLine: isThreeLine ?? this.isThreeLine,
);
}
@ -221,6 +226,7 @@ class ListTileThemeData with Diagnosticable {
visualDensity: t < 0.5 ? a?.visualDensity : b?.visualDensity,
titleAlignment: t < 0.5 ? a?.titleAlignment : b?.titleAlignment,
controlAffinity: t < 0.5 ? a?.controlAffinity : b?.controlAffinity,
isThreeLine: t < 0.5 ? a?.isThreeLine : b?.isThreeLine,
);
}
@ -247,6 +253,7 @@ class ListTileThemeData with Diagnosticable {
visualDensity,
titleAlignment,
controlAffinity,
isThreeLine,
]);
@override
@ -278,7 +285,8 @@ class ListTileThemeData with Diagnosticable {
other.mouseCursor == mouseCursor &&
other.visualDensity == visualDensity &&
other.titleAlignment == titleAlignment &&
other.controlAffinity == controlAffinity;
other.controlAffinity == controlAffinity &&
other.isThreeLine == isThreeLine;
}
@override
@ -337,6 +345,7 @@ class ListTileThemeData with Diagnosticable {
defaultValue: null,
),
);
properties.add(DiagnosticsProperty<bool>('isThreeLine', isThreeLine, defaultValue: null));
}
}
@ -573,6 +582,7 @@ class ListTileTheme extends InheritedTheme {
MaterialStateProperty<MouseCursor?>? mouseCursor,
VisualDensity? visualDensity,
ListTileControlAffinity? controlAffinity,
bool? isThreeLine,
required Widget child,
}) {
return Builder(
@ -603,6 +613,7 @@ class ListTileTheme extends InheritedTheme {
mouseCursor: mouseCursor ?? parent.mouseCursor,
visualDensity: visualDensity ?? parent.visualDensity,
controlAffinity: controlAffinity ?? parent.controlAffinity,
isThreeLine: isThreeLine ?? parent.isThreeLine,
),
child: child,
);
@ -627,6 +638,7 @@ class ListTileTheme extends InheritedTheme {
horizontalTitleGap: horizontalTitleGap,
minVerticalPadding: minVerticalPadding,
minLeadingWidth: minLeadingWidth,
isThreeLine: _data?.isThreeLine,
),
child: child,
);

View File

@ -42,6 +42,14 @@ abstract class RenderProxySliver extends RenderSliver
this.child = child;
}
@override
Rect get semanticBounds {
if (child != null) {
return child!.semanticBounds;
}
return super.semanticBounds;
}
@override
void setupParentData(RenderObject child) {
if (child.parentData is! SliverPhysicalParentData) {

View File

@ -5,6 +5,7 @@
/// @docImport 'package:flutter/material.dart';
///
/// @docImport 'proxy_box.dart';
/// @docImport 'proxy_sliver.dart';
/// @docImport 'sliver_fill.dart';
/// @docImport 'sliver_grid.dart';
/// @docImport 'sliver_list.dart';
@ -1306,6 +1307,28 @@ List<DiagnosticsNode> _debugCompareFloats(
/// than zero, then it should override [childCrossAxisPosition]. For example
/// [RenderSliverGrid] overrides this method.
abstract class RenderSliver extends RenderObject {
/// Whether this sliver should be included in the semantics tree.
///
/// This value is used by [RenderViewportBase] to ensure a sliver is
/// included in the semantics tree regardless of its geometry.
///
/// A [RenderSliver] should override this value to `true` to ensure
/// its child is included in the semantics tree. For example if your
/// sliver is under a [RenderViewport] you may want to wrap it with
/// a [SliverEnsureSemantics] to ensure that:
///
/// 1. It is still visited by [RenderViewportBase.visitChildrenForSemantics]
/// regardless of its geometry. This includes cases where your sliver is outside
/// the current viewport and cache extent.
/// 2. Its semantic information is not clipped out by the [RenderViewport] in
/// [RenderViewportBase.describeSemanticsClip] or [RenderViewportBase.describeApproximatePaintClip].
///
/// If a given [RenderSliver] does not provide a valid [semanticBounds] it will still
/// be dropped from the semantics tree.
///
/// Defaults to `false`.
bool get ensureSemantics => false;
// layout input
@override
SliverConstraints get constraints => super.constraints as SliverConstraints;

View File

@ -425,6 +425,18 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
// Do not visit children in [_keepAliveBucket].
}
@override
Rect get semanticBounds {
// If we laid out the first child but this sliver is not visible, we report the
// semantic bounds of this sliver as the bounds of the first child. This is necessary
// for accessibility technologies to reach this sliver even when it is outside
// the current viewport and cache extent.
if (geometry != null && !geometry!.visible && firstChild != null && firstChild!.hasSize) {
return firstChild!.paintBounds;
}
return super.semanticBounds;
}
/// Called during layout to create and add the child with the given index and
/// scroll offset.
///

View File

@ -332,7 +332,8 @@ class RenderTreeSliver extends RenderSliverVariedExtentList {
while (child != null && indexOf(child) <= index) {
final double mainAxisDelta = childMainAxisPosition(child);
final TreeSliverNodeParentData parentData = child.parentData! as TreeSliverNodeParentData;
final Offset childOffset = Offset(parentData.depth * indentation, parentData.layoutOffset!);
final Offset childOffset =
Offset(parentData.depth * indentation, parentData.layoutOffset!) + offset;
// If the child's visible interval (mainAxisDelta, mainAxisDelta + paintExtentOf(child))
// does not intersect the paint extent interval (0, constraints.remainingPaintExtent), it's hidden.

View File

@ -314,7 +314,10 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
childrenInPaintOrder
.where(
(RenderSliver sliver) => sliver.geometry!.visible || sliver.geometry!.cacheExtent > 0.0,
(RenderSliver sliver) =>
sliver.geometry!.visible ||
sliver.geometry!.cacheExtent > 0.0 ||
sliver.ensureSemantics,
)
.forEach(visitor);
}
@ -671,6 +674,12 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
@override
Rect? describeApproximatePaintClip(RenderSliver child) {
if (child.ensureSemantics && !(child.geometry!.visible || child.geometry!.cacheExtent > 0.0)) {
// Return null here so we don't end up clipping out a semantics node rect
// for a sliver child when we explicitly want it to be included in the semantics tree.
return null;
}
switch (clipBehavior) {
case Clip.none:
return null;
@ -716,7 +725,14 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
}
@override
Rect describeSemanticsClip(RenderSliver? child) {
Rect? describeSemanticsClip(RenderSliver? child) {
if (child != null &&
child.ensureSemantics &&
!(child.geometry!.visible || child.geometry!.cacheExtent > 0.0)) {
// Return null here so we don't end up clipping out a semantics node rect
// for a sliver child when we explicitly want it to be included in the semantics tree.
return null;
}
if (_calculatedCacheExtent == null) {
return semanticBounds;
}

View File

@ -1771,3 +1771,48 @@ class _SliverMainAxisGroupElement extends MultiChildRenderObjectElement {
.forEach(visitor);
}
}
/// A sliver that ensures its sliver child is included in the semantics tree.
///
/// This sliver ensures that its child sliver is still visited by the [RenderViewport]
/// when constructing the semantics tree, and is not clipped out of the semantics tree by
/// the [RenderViewport] when it is outside the current viewport and outside the cache extent.
///
/// The child sliver may still be excluded from the semantics tree if its [RenderSliver] does
/// not provide a valid [RenderSliver.semanticBounds]. This sliver does not guarantee its
/// child sliver is laid out.
///
/// Be mindful when positioning [SliverEnsureSemantics] in a [CustomScrollView] after slivers that build
/// their children lazily, like [SliverList]. Lazy slivers might underestimate the total scrollable size (scroll
/// extent) before the [SliverEnsureSemantics] widget. This inaccuracy can cause problems for assistive
/// technologies (e.g., screen readers), which rely on a correct scroll extent to navigate properly; they
/// might fail to scroll accurately to the content wrapped by [SliverEnsureSemantics].
///
/// To avoid this potential issue and ensure the scroll extent is calculated accurately up to this sliver,
/// it's recommended to use slivers that can determine their extent precisely beforehand. Instead of
/// [SliverList], consider using [SliverFixedExtentList], [SliverVariedExtentList], or
/// [SliverPrototypeExtentList]. If using [SliverGrid], ensure it employs a delegate such as
/// [SliverGridDelegateWithFixedCrossAxisCount] or [SliverGridDelegateWithMaxCrossAxisExtent].
/// Using these alternatives guarantees that the scrollable area's size is known accurately, allowing
/// assistive technologies to function correctly with [SliverEnsureSemantics].
///
/// {@tool dartpad}
/// This example shows how to use [SliverEnsureSemantics] to keep certain headers and lists
/// available to assistive technologies while they are outside the current viewport and cache extent.
///
/// ** See code in examples/api/lib/widgets/sliver/sliver_ensure_semantics.0.dart **
/// {@end-tool}
// TODO(Renzo-Olivares): Investigate potential solutions for revealing off screen items, https://github.com/flutter/flutter/issues/166703.
class SliverEnsureSemantics extends SingleChildRenderObjectWidget {
/// Creates a sliver that ensures its sliver child is included in the semantics tree.
const SliverEnsureSemantics({super.key, required Widget sliver}) : super(child: sliver);
@override
RenderObject createRenderObject(BuildContext context) => _RenderSliverEnsureSemantics();
}
/// Ensures its sliver child is included in the semantics tree.
class _RenderSliverEnsureSemantics extends RenderProxySliver {
@override
bool get ensureSemantics => true;
}

View File

@ -17,8 +17,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import '../impeller_test_helpers.dart';
// TODO(yjbanov): on the web text rendered with perspective produces flaky goldens: https://github.com/flutter/flutter/issues/110785
final bool skipPerspectiveTextGoldens = isBrowser && isSkwasm;
@ -1611,7 +1609,7 @@ void main() {
matchesGoldenFile('date_picker_test.datetime.drag.png'),
);
}
}, skip: impellerEnabled); // https://github.com/flutter/flutter/issues/143616
});
testWidgets('DatePicker displays the date in correct order', (WidgetTester tester) async {
await tester.pumpWidget(
@ -1761,7 +1759,7 @@ void main() {
matchesGoldenFile('timer_picker_test.datetime.drag.png'),
);
}
}, skip: impellerEnabled); // https://github.com/flutter/flutter/issues/143616
});
testWidgets('TimerPicker only changes hour label after scrolling stops', (
WidgetTester tester,

View File

@ -4361,6 +4361,138 @@ void main() {
expect(trailingOffset.dy - tileOffset.dy, minVerticalPadding);
});
});
// Regression test for https://github.com/flutter/flutter/issues/165453
testWidgets('ListTile isThreeLine', (WidgetTester tester) async {
const double height = 300;
const double avatarTop = 130.0;
const double placeholderTop = 138.0;
Widget buildFrame({bool? themeDataIsThreeLine, bool? themeIsThreeLine, bool? isThreeLine}) {
return MaterialApp(
key: UniqueKey(),
theme:
themeDataIsThreeLine != null
? ThemeData(listTileTheme: ListTileThemeData(isThreeLine: themeDataIsThreeLine))
: null,
home: Material(
child: ListTileTheme(
data:
themeIsThreeLine != null ? ListTileThemeData(isThreeLine: themeIsThreeLine) : null,
child: ListView(
children: <Widget>[
ListTile(
isThreeLine: isThreeLine,
leading: const CircleAvatar(),
trailing: const SizedBox(height: 24.0, width: 24.0, child: Placeholder()),
title: const Text('A'),
subtitle: const Text('A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM'),
),
ListTile(
isThreeLine: isThreeLine,
leading: const CircleAvatar(),
trailing: const SizedBox(height: 24.0, width: 24.0, child: Placeholder()),
title: const Text('A'),
subtitle: const Text('A'),
),
],
),
),
),
);
}
void expectTwoLine() {
expect(
tester.getRect(find.byType(ListTile).at(0)),
const Rect.fromLTWH(0.0, 0.0, 800.0, height),
);
expect(
tester.getRect(find.byType(CircleAvatar).at(0)),
const Rect.fromLTWH(16.0, avatarTop, 40.0, 40.0),
);
expect(
tester.getRect(find.byType(Placeholder).at(0)),
const Rect.fromLTWH(800.0 - 24.0 - 24.0, placeholderTop, 24.0, 24.0),
);
expect(
tester.getRect(find.byType(ListTile).at(1)),
const Rect.fromLTWH(0.0, height, 800.0, 72.0),
);
expect(
tester.getRect(find.byType(CircleAvatar).at(1)),
const Rect.fromLTWH(16.0, height + 16.0, 40.0, 40.0),
);
expect(
tester.getRect(find.byType(Placeholder).at(1)),
const Rect.fromLTWH(800.0 - 24.0 - 24.0, height + 24.0, 24.0, 24.0),
);
}
void expectThreeLine() {
expect(
tester.getRect(find.byType(ListTile).at(0)),
const Rect.fromLTWH(0.0, 0.0, 800.0, height),
);
expect(
tester.getRect(find.byType(CircleAvatar).at(0)),
const Rect.fromLTWH(16.0, 8.0, 40.0, 40.0),
);
expect(
tester.getRect(find.byType(Placeholder).at(0)),
const Rect.fromLTWH(800.0 - 24.0 - 24.0, 8.0, 24.0, 24.0),
);
expect(
tester.getRect(find.byType(ListTile).at(1)),
const Rect.fromLTWH(0.0, height, 800.0, 88.0),
);
expect(
tester.getRect(find.byType(CircleAvatar).at(1)),
const Rect.fromLTWH(16.0, height + 8.0, 40.0, 40.0),
);
expect(
tester.getRect(find.byType(Placeholder).at(1)),
const Rect.fromLTWH(800.0 - 24.0 - 24.0, height + 8.0, 24.0, 24.0),
);
}
await tester.pumpWidget(buildFrame());
expectTwoLine();
await tester.pumpWidget(buildFrame(themeDataIsThreeLine: true));
expectThreeLine();
await tester.pumpWidget(buildFrame(themeDataIsThreeLine: false, themeIsThreeLine: true));
expectThreeLine();
await tester.pumpWidget(buildFrame(themeDataIsThreeLine: true, themeIsThreeLine: false));
expectTwoLine();
await tester.pumpWidget(buildFrame(isThreeLine: true));
expectThreeLine();
await tester.pumpWidget(buildFrame(themeIsThreeLine: true, isThreeLine: false));
expectTwoLine();
await tester.pumpWidget(buildFrame(themeDataIsThreeLine: true, isThreeLine: false));
expectTwoLine();
await tester.pumpWidget(
buildFrame(themeDataIsThreeLine: true, themeIsThreeLine: true, isThreeLine: false),
);
expectTwoLine();
await tester.pumpWidget(buildFrame(themeIsThreeLine: false, isThreeLine: true));
expectThreeLine();
await tester.pumpWidget(buildFrame(themeDataIsThreeLine: false, isThreeLine: true));
expectThreeLine();
await tester.pumpWidget(
buildFrame(themeDataIsThreeLine: false, themeIsThreeLine: false, isThreeLine: true),
);
expectThreeLine();
});
}
RenderParagraph _getTextRenderObject(WidgetTester tester, String text) {

View File

@ -77,6 +77,7 @@ void main() {
expect(themeData.mouseCursor, null);
expect(themeData.visualDensity, null);
expect(themeData.titleAlignment, null);
expect(themeData.isThreeLine, null);
});
testWidgets('Default ListTileThemeData debugFillProperties', (WidgetTester tester) async {
@ -115,6 +116,7 @@ void main() {
mouseCursor: MaterialStateMouseCursor.clickable,
visualDensity: VisualDensity.comfortable,
titleAlignment: ListTileTitleAlignment.top,
isThreeLine: true,
).debugFillProperties(builder);
final List<String> description =
@ -146,6 +148,7 @@ void main() {
'mouseCursor: WidgetStateMouseCursor(clickable)',
'visualDensity: VisualDensity#00000(h: -1.0, v: -1.0)(horizontal: -1.0, vertical: -1.0)',
'titleAlignment: ListTileTitleAlignment.top',
'isThreeLine: true',
]),
);
});
@ -937,6 +940,7 @@ void main() {
minTileHeight: 30,
enableFeedback: true,
titleAlignment: ListTileTitleAlignment.bottom,
isThreeLine: true,
);
final ListTileThemeData copy = original.copyWith(
@ -958,6 +962,7 @@ void main() {
minTileHeight: 80,
enableFeedback: false,
titleAlignment: ListTileTitleAlignment.top,
isThreeLine: false,
);
expect(copy.dense, false);
@ -978,6 +983,7 @@ void main() {
expect(copy.minTileHeight, 80);
expect(copy.enableFeedback, false);
expect(copy.titleAlignment, ListTileTitleAlignment.top);
expect(copy.isThreeLine, false);
});
testWidgets('ListTileTheme.titleAlignment is overridden by ListTile.titleAlignment', (
@ -1040,6 +1046,7 @@ void main() {
titleAlignment: ListTileTitleAlignment.bottom,
mouseCursor: MaterialStateMouseCursor.textable,
visualDensity: VisualDensity.comfortable,
isThreeLine: true,
),
),
home: Material(
@ -1067,6 +1074,7 @@ void main() {
titleAlignment: ListTileTitleAlignment.top,
mouseCursor: MaterialStateMouseCursor.clickable,
visualDensity: VisualDensity.compact,
isThreeLine: false,
child: const ListTile(),
);
},
@ -1098,6 +1106,217 @@ void main() {
expect(theme.titleAlignment, ListTileTitleAlignment.top);
expect(theme.mouseCursor, MaterialStateMouseCursor.clickable);
expect(theme.visualDensity, VisualDensity.compact);
expect(theme.isThreeLine, false);
});
// Regression test for https://github.com/flutter/flutter/issues/165453
testWidgets('ListTileThemeData isThreeLine', (WidgetTester tester) async {
const double height = 300;
const double avatarTop = 130.0;
const double placeholderTop = 138.0;
Widget buildFrame({bool? isThreeLine}) {
return MaterialApp(
key: UniqueKey(),
theme:
isThreeLine != null
? ThemeData(listTileTheme: ListTileThemeData(isThreeLine: isThreeLine))
: null,
home: Material(
child: ListView(
children: const <Widget>[
ListTile(
leading: CircleAvatar(),
trailing: SizedBox(height: 24.0, width: 24.0, child: Placeholder()),
title: Text('A'),
subtitle: Text('A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM'),
),
ListTile(
leading: CircleAvatar(),
trailing: SizedBox(height: 24.0, width: 24.0, child: Placeholder()),
title: Text('A'),
subtitle: Text('A'),
),
],
),
),
);
}
void expectTwoLine() {
expect(
tester.getRect(find.byType(ListTile).at(0)),
const Rect.fromLTWH(0.0, 0.0, 800.0, height),
);
expect(
tester.getRect(find.byType(CircleAvatar).at(0)),
const Rect.fromLTWH(16.0, avatarTop, 40.0, 40.0),
);
expect(
tester.getRect(find.byType(Placeholder).at(0)),
const Rect.fromLTWH(800.0 - 24.0 - 24.0, placeholderTop, 24.0, 24.0),
);
expect(
tester.getRect(find.byType(ListTile).at(1)),
const Rect.fromLTWH(0.0, height, 800.0, 72.0),
);
expect(
tester.getRect(find.byType(CircleAvatar).at(1)),
const Rect.fromLTWH(16.0, height + 16.0, 40.0, 40.0),
);
expect(
tester.getRect(find.byType(Placeholder).at(1)),
const Rect.fromLTWH(800.0 - 24.0 - 24.0, height + 24.0, 24.0, 24.0),
);
}
void expectThreeLine() {
expect(
tester.getRect(find.byType(ListTile).at(0)),
const Rect.fromLTWH(0.0, 0.0, 800.0, height),
);
expect(
tester.getRect(find.byType(CircleAvatar).at(0)),
const Rect.fromLTWH(16.0, 8.0, 40.0, 40.0),
);
expect(
tester.getRect(find.byType(Placeholder).at(0)),
const Rect.fromLTWH(800.0 - 24.0 - 24.0, 8.0, 24.0, 24.0),
);
expect(
tester.getRect(find.byType(ListTile).at(1)),
const Rect.fromLTWH(0.0, height, 800.0, 88.0),
);
expect(
tester.getRect(find.byType(CircleAvatar).at(1)),
const Rect.fromLTWH(16.0, height + 8.0, 40.0, 40.0),
);
expect(
tester.getRect(find.byType(Placeholder).at(1)),
const Rect.fromLTWH(800.0 - 24.0 - 24.0, height + 8.0, 24.0, 24.0),
);
}
await tester.pumpWidget(buildFrame());
expectTwoLine();
await tester.pumpWidget(buildFrame(isThreeLine: false));
expectTwoLine();
await tester.pumpWidget(buildFrame(isThreeLine: true));
expectThreeLine();
});
// Regression test for https://github.com/flutter/flutter/issues/165453
testWidgets('ListTileTheme isThreeLine', (WidgetTester tester) async {
const double height = 300;
const double avatarTop = 130.0;
const double placeholderTop = 138.0;
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(listTileTheme: const ListTileThemeData(isThreeLine: true)),
home: Material(
child: ListTileTheme(
data: const ListTileThemeData(isThreeLine: false),
child: ListView(
children: const <Widget>[
ListTile(
leading: CircleAvatar(),
trailing: SizedBox(height: 24.0, width: 24.0, child: Placeholder()),
title: Text('A'),
subtitle: Text('A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM'),
),
ListTile(
leading: CircleAvatar(),
trailing: SizedBox(height: 24.0, width: 24.0, child: Placeholder()),
title: Text('A'),
subtitle: Text('A'),
),
],
),
),
),
),
);
expect(
tester.getRect(find.byType(ListTile).at(0)),
const Rect.fromLTWH(0.0, 0.0, 800.0, height),
);
expect(
tester.getRect(find.byType(CircleAvatar).at(0)),
const Rect.fromLTWH(16.0, avatarTop, 40.0, 40.0),
);
expect(
tester.getRect(find.byType(Placeholder).at(0)),
const Rect.fromLTWH(800.0 - 24.0 - 24.0, placeholderTop, 24.0, 24.0),
);
expect(
tester.getRect(find.byType(ListTile).at(1)),
const Rect.fromLTWH(0.0, height, 800.0, 72.0),
);
expect(
tester.getRect(find.byType(CircleAvatar).at(1)),
const Rect.fromLTWH(16.0, height + 16.0, 40.0, 40.0),
);
expect(
tester.getRect(find.byType(Placeholder).at(1)),
const Rect.fromLTWH(800.0 - 24.0 - 24.0, height + 24.0, 24.0, 24.0),
);
// THREE-LINE
await tester.pumpWidget(
MaterialApp(
key: UniqueKey(),
home: Material(
child: ListTileTheme(
data: const ListTileThemeData(isThreeLine: true),
child: ListView(
children: const <Widget>[
ListTile(
leading: CircleAvatar(),
trailing: SizedBox(height: 24.0, width: 24.0, child: Placeholder()),
title: Text('A'),
subtitle: Text('A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM'),
),
ListTile(
leading: CircleAvatar(),
trailing: SizedBox(height: 24.0, width: 24.0, child: Placeholder()),
title: Text('A'),
subtitle: Text('A'),
),
],
),
),
),
),
);
expect(
tester.getRect(find.byType(ListTile).at(0)),
const Rect.fromLTWH(0.0, 0.0, 800.0, height),
);
expect(
tester.getRect(find.byType(CircleAvatar).at(0)),
const Rect.fromLTWH(16.0, 8.0, 40.0, 40.0),
);
expect(
tester.getRect(find.byType(Placeholder).at(0)),
const Rect.fromLTWH(800.0 - 24.0 - 24.0, 8.0, 24.0, 24.0),
);
expect(
tester.getRect(find.byType(ListTile).at(1)),
const Rect.fromLTWH(0.0, height, 800.0, 88.0),
);
expect(
tester.getRect(find.byType(CircleAvatar).at(1)),
const Rect.fromLTWH(16.0, height + 8.0, 40.0, 40.0),
);
expect(
tester.getRect(find.byType(Placeholder).at(1)),
const Rect.fromLTWH(800.0 - 24.0 - 24.0, height + 8.0, 24.0, 24.0),
);
});
}

View File

@ -12,8 +12,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import '../impeller_test_helpers.dart';
void main() {
testWidgets('Color filter - red', (WidgetTester tester) async {
await tester.pumpWidget(
@ -56,7 +54,7 @@ void main() {
),
);
await expectLater(find.byType(ColorFiltered), matchesGoldenFile('color_filter_sepia.png'));
}, skip: impellerEnabled); // https://github.com/flutter/flutter/issues/143616
});
testWidgets('Color filter - reuses its layer', (WidgetTester tester) async {
Future<void> pumpWithColor(Color color) async {

View File

@ -20,7 +20,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
import '../impeller_test_helpers.dart';
import '../widgets/clipboard_utils.dart';
import '../widgets/editable_text_utils.dart' show textOffsetToPosition;
import '../widgets/semantics_tester.dart';
@ -5267,7 +5266,7 @@ void main() {
find.byType(MaterialApp),
matchesGoldenFile('selectable_text_golden.TextSelectionStyle.1.png'),
);
}, skip: impellerEnabled); // https://github.com/flutter/flutter/issues/143616
});
testWidgets('text selection style 2', (WidgetTester tester) async {
await tester.pumpWidget(
@ -5304,7 +5303,7 @@ void main() {
find.byType(MaterialApp),
matchesGoldenFile('selectable_text_golden.TextSelectionStyle.2.png'),
);
}, skip: impellerEnabled); // https://github.com/flutter/flutter/issues/143616
});
testWidgets('keeps alive when has focus', (WidgetTester tester) async {
await tester.pumpWidget(

View File

@ -10,8 +10,6 @@ library;
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../impeller_test_helpers.dart';
Shader createShader(Rect bounds) {
return const LinearGradient(
begin: Alignment.topCenter,
@ -102,5 +100,5 @@ void main() {
find.byType(RepaintBoundary),
matchesGoldenFile('shader_mask.bounds.matches_top_left.png'),
);
}, skip: impellerEnabled); // https://github.com/flutter/flutter/issues/144555
});
}

View File

@ -2,6 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This file is run as part of a reduced test set in CI on Mac and Windows
// machines.
@Tags(<String>['reduced-test-set'])
library;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
@ -861,4 +866,43 @@ void main() {
});
},
);
testWidgets('TreeSliver and PinnedHeaderSliver can render correctly when used together.', (
WidgetTester tester,
) async {
const ValueKey<String> key = ValueKey<String>('sliver_tree_pined_header');
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Align(
alignment: Alignment.topLeft,
child: RepaintBoundary(
key: key,
child: SizedBox(
height: 20,
width: 20,
child: CustomScrollView(
slivers: <Widget>[
const PinnedHeaderSliver(child: SizedBox(height: 10)),
TreeSliver<Object>(
tree: <TreeSliverNode<Object>>[TreeSliverNode<Object>(Object())],
treeRowExtentBuilder: (_, _) => 10,
treeNodeBuilder: (
BuildContext context,
TreeSliverNode<Object?> node,
AnimationStyle animationStyle,
) {
return const ColoredBox(color: Colors.red);
},
),
],
),
),
),
),
),
);
await expectLater(find.byKey(key), matchesGoldenFile('sliver_tree.pined_header.0.png'));
expect(tester.getTopLeft(find.byType(ColoredBox)), const Offset(0, 10));
});
}

View File

@ -726,7 +726,7 @@ void main() {
},
);
Widget boilerPlate(Widget sliver) {
Widget boilerPlate(List<Widget> slivers) {
return Localizations(
locale: const Locale('en', 'us'),
delegates: const <LocalizationsDelegate<dynamic>>[
@ -735,10 +735,7 @@ void main() {
],
child: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: CustomScrollView(slivers: <Widget>[sliver]),
),
child: MediaQuery(data: const MediaQueryData(), child: CustomScrollView(slivers: slivers)),
),
);
}
@ -747,7 +744,7 @@ void main() {
testWidgets('offstage true', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
boilerPlate(const SliverOffstage(sliver: SliverToBoxAdapter(child: Text('a')))),
boilerPlate(<Widget>[const SliverOffstage(sliver: SliverToBoxAdapter(child: Text('a')))]),
);
expect(semantics.nodesWith(label: 'a'), hasLength(0));
@ -762,9 +759,9 @@ void main() {
testWidgets('offstage false', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
boilerPlate(
boilerPlate(<Widget>[
const SliverOffstage(offstage: false, sliver: SliverToBoxAdapter(child: Text('a'))),
),
]),
);
expect(semantics.nodesWith(label: 'a'), hasLength(1));
@ -783,12 +780,12 @@ void main() {
// Opacity 1.0: Semantics and painting
await tester.pumpWidget(
boilerPlate(
boilerPlate(<Widget>[
const SliverOpacity(
sliver: SliverToBoxAdapter(child: Text('a', textDirection: TextDirection.rtl)),
opacity: 1.0,
),
),
]),
);
expect(semantics.nodesWith(label: 'a'), hasLength(1));
@ -796,12 +793,12 @@ void main() {
// Opacity 0.0: Nothing
await tester.pumpWidget(
boilerPlate(
boilerPlate(<Widget>[
const SliverOpacity(
sliver: SliverToBoxAdapter(child: Text('a', textDirection: TextDirection.rtl)),
opacity: 0.0,
),
),
]),
);
expect(semantics.nodesWith(label: 'a'), hasLength(0));
@ -809,13 +806,13 @@ void main() {
// Opacity 0.0 with semantics: Just semantics
await tester.pumpWidget(
boilerPlate(
boilerPlate(<Widget>[
const SliverOpacity(
sliver: SliverToBoxAdapter(child: Text('a', textDirection: TextDirection.rtl)),
opacity: 0.0,
alwaysIncludeSemantics: true,
),
),
]),
);
expect(semantics.nodesWith(label: 'a'), hasLength(1));
@ -823,12 +820,12 @@ void main() {
// Opacity 0.0 without semantics: Nothing
await tester.pumpWidget(
boilerPlate(
boilerPlate(<Widget>[
const SliverOpacity(
sliver: SliverToBoxAdapter(child: Text('a', textDirection: TextDirection.rtl)),
opacity: 0.0,
),
),
]),
);
expect(semantics.nodesWith(label: 'a'), hasLength(0));
@ -836,12 +833,12 @@ void main() {
// Opacity 0.1: Semantics and painting
await tester.pumpWidget(
boilerPlate(
boilerPlate(<Widget>[
const SliverOpacity(
sliver: SliverToBoxAdapter(child: Text('a', textDirection: TextDirection.rtl)),
opacity: 0.1,
),
),
]),
);
expect(semantics.nodesWith(label: 'a'), hasLength(1));
@ -849,12 +846,12 @@ void main() {
// Opacity 0.1 without semantics: Still has semantics and painting
await tester.pumpWidget(
boilerPlate(
boilerPlate(<Widget>[
const SliverOpacity(
sliver: SliverToBoxAdapter(child: Text('a', textDirection: TextDirection.rtl)),
opacity: 0.1,
),
),
]),
);
expect(semantics.nodesWith(label: 'a'), hasLength(1));
@ -862,13 +859,13 @@ void main() {
// Opacity 0.1 with semantics: Semantics and painting
await tester.pumpWidget(
boilerPlate(
boilerPlate(<Widget>[
const SliverOpacity(
sliver: SliverToBoxAdapter(child: Text('a', textDirection: TextDirection.rtl)),
opacity: 0.1,
alwaysIncludeSemantics: true,
),
),
]),
);
expect(semantics.nodesWith(label: 'a'), hasLength(1));
@ -883,7 +880,7 @@ void main() {
final SemanticsTester semantics = SemanticsTester(tester);
final List<String> events = <String>[];
await tester.pumpWidget(
boilerPlate(
boilerPlate(<Widget>[
SliverIgnorePointer(
ignoringSemantics: false,
sliver: SliverToBoxAdapter(
@ -895,7 +892,7 @@ void main() {
),
),
),
),
]),
);
expect(semantics.nodesWith(label: 'a'), hasLength(1));
await tester.tap(find.byType(GestureDetector), warnIfMissed: false);
@ -907,7 +904,7 @@ void main() {
final SemanticsTester semantics = SemanticsTester(tester);
final List<String> events = <String>[];
await tester.pumpWidget(
boilerPlate(
boilerPlate(<Widget>[
SliverIgnorePointer(
ignoring: false,
ignoringSemantics: true,
@ -920,7 +917,7 @@ void main() {
),
),
),
),
]),
);
expect(semantics.nodesWith(label: 'a'), hasLength(0));
await tester.tap(find.byType(GestureDetector));
@ -931,13 +928,13 @@ void main() {
testWidgets('ignoring only block semantics actions', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
boilerPlate(
boilerPlate(<Widget>[
SliverIgnorePointer(
sliver: SliverToBoxAdapter(
child: GestureDetector(child: const Text('a'), onTap: () {}),
),
),
),
]),
);
expect(semantics, includesNodeWith(label: 'a', actions: <SemanticsAction>[]));
semantics.dispose();
@ -947,7 +944,7 @@ void main() {
final SemanticsTester semantics = SemanticsTester(tester);
final List<String> events = <String>[];
await tester.pumpWidget(
boilerPlate(
boilerPlate(<Widget>[
SliverIgnorePointer(
ignoringSemantics: true,
sliver: SliverToBoxAdapter(
@ -959,7 +956,7 @@ void main() {
),
),
),
),
]),
);
expect(semantics.nodesWith(label: 'a'), hasLength(0));
await tester.tap(find.byType(GestureDetector), warnIfMissed: false);
@ -971,7 +968,7 @@ void main() {
final SemanticsTester semantics = SemanticsTester(tester);
final List<String> events = <String>[];
await tester.pumpWidget(
boilerPlate(
boilerPlate(<Widget>[
SliverIgnorePointer(
ignoring: false,
ignoringSemantics: false,
@ -984,7 +981,7 @@ void main() {
),
),
),
),
]),
);
expect(semantics.nodesWith(label: 'a'), hasLength(1));
await tester.tap(find.byType(GestureDetector));
@ -993,6 +990,40 @@ void main() {
});
});
group('SliverEnsureSemantics - ', () {
testWidgets('ensure semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
boilerPlate(<Widget>[
const SliverEnsureSemantics(sliver: SliverToBoxAdapter(child: Text('a'))),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text('Lorem Ipsum $index'),
),
);
},
childCount: 50,
semanticIndexOffset: 1,
),
),
const SliverEnsureSemantics(sliver: SliverToBoxAdapter(child: Text('b'))),
]),
);
// Even though 'b' is outside of the Viewport and cacheExtent, since it is
// wrapped with a `SliverEnsureSemantics` it will still be included in the
// semantics tree.
expect(semantics.nodesWith(label: 'b'), hasLength(1));
expect(find.text('b'), findsNothing);
expect(find.byType(SliverEnsureSemantics, skipOffstage: false), findsNWidgets(2));
semantics.dispose();
});
});
testWidgets('SliverList handles 0 scrollOffsetCorrection', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/62198
await tester.pumpWidget(

View File

@ -258,8 +258,6 @@ List<FlutterCommand> generateCommands({required bool verboseHelp, required bool
platform: globals.platform,
shutdownHooks: globals.shutdownHooks,
os: globals.os,
processManager: globals.processManager,
artifacts: globals.artifacts!,
),
UpgradeCommand(verboseHelp: verboseHelp),
SymbolizeCommand(stdio: globals.stdio, fileSystem: globals.fs),

View File

@ -5,9 +5,7 @@
import 'package:args/args.dart';
import 'package:meta/meta.dart';
import 'package:package_config/package_config.dart';
import 'package:process/process.dart';
import '../artifacts.dart';
import '../base/common.dart';
import '../base/deferred_component.dart';
import '../base/file_system.dart';
@ -26,8 +24,6 @@ import '../linux/build_linux.dart';
import '../macos/build_macos.dart';
import '../project.dart';
import '../runner/flutter_command.dart';
import '../runner/flutter_command_runner.dart';
import '../widget_preview/dtd_services.dart';
import '../widget_preview/preview_code_generator.dart';
import '../widget_preview/preview_detector.dart';
import '../widget_preview/preview_manifest.dart';
@ -45,8 +41,6 @@ class WidgetPreviewCommand extends FlutterCommand {
required Platform platform,
required ShutdownHooks shutdownHooks,
required OperatingSystemUtils os,
required ProcessManager processManager,
required Artifacts artifacts,
}) {
addSubcommand(
WidgetPreviewStartCommand(
@ -58,8 +52,6 @@ class WidgetPreviewCommand extends FlutterCommand {
platform: platform,
shutdownHooks: shutdownHooks,
os: os,
processManager: processManager,
artifacts: artifacts,
),
);
addSubcommand(
@ -126,8 +118,6 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
required this.platform,
required this.shutdownHooks,
required this.os,
required this.processManager,
required this.artifacts,
}) {
addPubOptions();
argParser
@ -162,9 +152,6 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
static const String kHeadlessWeb = 'headless-web';
static const String kWidgetPreviewScaffoldOutputDir = 'scaffold-output-dir';
/// Environment variable used to pass the DTD URI to the widget preview scaffold.
static const String kWidgetPreviewDtdUriEnvVar = 'WIDGET_PREVIEW_DTD_URI';
@override
Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{
// Ensure the Flutter Web SDK is installed.
@ -198,10 +185,6 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
final OperatingSystemUtils os;
final ProcessManager processManager;
final Artifacts artifacts;
late final FlutterProject rootProject = getRootProject();
late final PreviewDetector _previewDetector = PreviewDetector(
@ -220,12 +203,6 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
cache: cache,
);
late final WidgetPreviewDtdServices _dtdService = WidgetPreviewDtdServices(
logger: logger,
shutdownHooks: shutdownHooks,
dtdLauncher: DtdLauncher(logger: logger, artifacts: artifacts, processManager: processManager),
);
/// The currently running instance of the widget preview scaffold.
AppInstance? _widgetPreviewApp;
@ -307,7 +284,6 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
shutdownHooks.addShutdownHook(() async {
await _widgetPreviewApp?.stop();
});
await configureDtd();
_widgetPreviewApp = await runPreviewEnvironment(
widgetPreviewScaffoldProject: rootProject.widgetPreviewScaffoldProject,
);
@ -333,31 +309,6 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
_populatePreviewPubspec(rootProject: rootProject);
}
/// Configures the Dart Tooling Daemon connection.
///
/// If --dtd-uri is provided, the existing DTD instance will be used. If the tool fails to
/// connect to this URI, it will start its own DTD instance.
///
/// If --dtd-uri is not provided, a DTD instance managed by the tool will be started.
Future<void> configureDtd() async {
final String? existingDtdUriStr = stringArg(FlutterGlobalOptions.kDtdUrl, global: true);
Uri? existingDtdUri;
try {
if (existingDtdUriStr != null) {
existingDtdUri = Uri.parse(existingDtdUriStr);
}
} on FormatException {
logger.printWarning('Failed to parse value of --dtd-uri: $existingDtdUriStr.');
}
if (existingDtdUri == null) {
logger.printTrace('Launching a fresh DTD instance...');
await _dtdService.launchAndConnect();
} else {
logger.printTrace('Connecting to existing DTD instance at: $existingDtdUri...');
await _dtdService.connect(dtdWsUri: existingDtdUri);
}
}
/// Builds the application binary for the widget preview scaffold the first
/// time the widget preview command is run.
///
@ -506,12 +457,6 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
BuildMode.debug,
null,
treeShakeIcons: false,
// Provide the DTD connection information directly to the preview scaffold.
// This could, in theory, be provided via a follow up call to a service extension
// registered by the preview scaffold, but there's some uncertainty around how service
// extensions will work with Flutter web embedded in VSCode without a Chrome debugger
// connection.
dartDefines: <String>['$kWidgetPreviewDtdUriEnvVar=${_dtdService.dtdUri}'],
extraFrontEndOptions:
isWeb ? <String>['--dartdevc-canary', '--dartdevc-module-format=ddc'] : null,
packageConfigPath: widgetPreviewScaffoldProject.packageConfig.path,
@ -654,7 +599,6 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
if (offline) '--offline',
'--directory',
widgetPreviewScaffoldProject.directory.path,
'dtd',
'flutter_lints',
'stack_trace',
],

View File

@ -18,7 +18,13 @@ Future<Set<String>> computeExclusiveDevDependencies(
required Logger logger,
required FlutterProject project,
}) async {
final Map<String, Object?> jsonResult = await pub.deps(project);
final Map<String, Object?>? jsonResult = await pub.deps(project);
// Avoid crashing if dart pub deps is not ready.
// See https://github.com/flutter/flutter/issues/166648.
if (jsonResult == null) {
return <String>{};
}
Never fail([String? reason]) {
logger.printTrace(const JsonEncoder.withIndent(' ').convert(jsonResult));

View File

@ -8,6 +8,7 @@ import 'package:meta/meta.dart';
import 'package:process/process.dart';
import '../application_package.dart';
import '../artifacts.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
@ -21,6 +22,7 @@ import '../convert.dart';
import '../device.dart';
import '../device_port_forwarder.dart';
import '../features.dart';
import '../globals.dart' as globals;
import '../project.dart';
import '../protocol_discovery.dart';
import '../vmservice.dart';
@ -341,6 +343,7 @@ class CustomDeviceAppSession {
Map<String, Object?> platformArgs = const <String, Object>{},
bool prebuiltApplication = false,
String? userIdentifier,
Map<String, String> additionalReplacementValues = const <String, String>{},
}) async {
final bool traceStartup = platformArgs['trace-startup'] as bool? ?? false;
final String? packageName = _appPackage.name;
@ -352,7 +355,7 @@ class CustomDeviceAppSession {
'remotePath': '/tmp/',
'appName': packageName,
'engineOptions': _getEngineOptionsForCmdline(debuggingOptions, traceStartup, route),
});
}, additionalReplacementValues: additionalReplacementValues);
final Process process = await _processUtils.start(interpolated);
assert(_process == null);
@ -700,6 +703,16 @@ class CustomDevice extends Device {
String? userIdentifier,
BundleBuilder? bundleBuilder,
}) async {
final TargetPlatform platform = await targetPlatform;
final Artifacts artifacts = globals.artifacts!;
final Map<String, String> additionalReplacementValues = <String, String>{
'buildMode': debuggingOptions.buildInfo.modeName,
'icuDataPath': artifacts.getArtifactPath(Artifact.icuData, platform: platform),
'engineRevision':
artifacts.usesLocalArtifacts ? 'local' : globals.flutterVersion.engineRevision,
};
if (!prebuiltApplication) {
final String assetBundleDir = getAssetBuildDirectory();
@ -707,7 +720,7 @@ class CustomDevice extends Device {
// this just builds the asset bundle, it's the same as `flutter build bundle`
await bundleBuilder.build(
platform: await targetPlatform,
platform: platform,
buildInfo: debuggingOptions.buildInfo,
mainPath: mainPath,
depfilePath: defaultDepfilePath,
@ -720,7 +733,11 @@ class CustomDevice extends Device {
if (packageName == null) {
throwToolExit('Could not start app, name for $package is unknown.');
}
await _tryPostBuild(appName: packageName, localPath: assetBundleDir);
await _tryPostBuild(
appName: packageName,
localPath: assetBundleDir,
additionalReplacementValues: additionalReplacementValues,
);
}
}
@ -736,6 +753,7 @@ class CustomDevice extends Device {
platformArgs: platformArgs,
prebuiltApplication: prebuiltApplication,
userIdentifier: userIdentifier,
additionalReplacementValues: additionalReplacementValues,
);
}

View File

@ -158,7 +158,9 @@ abstract class Pub {
/// While it is guaranteed that, if successful, that the result are a valid
/// JSON object, the exact contents returned are _not_ validated, and are left
/// as a responsibility of the caller.
Future<Map<String, Object?>> deps(FlutterProject project);
///
/// If `null` is returned, it should be assumed deps could not be determined.
Future<Map<String, Object?>?> deps(FlutterProject project);
/// Runs pub in 'batch' mode.
///
@ -354,13 +356,22 @@ class _DefaultPub implements Pub {
}
@override
Future<Map<String, Object?>> deps(FlutterProject project) async {
Future<Map<String, Object?>?> deps(FlutterProject project) async {
final List<String> pubCommand = <String>[..._pubCommand, 'deps', '--json'];
final RunResult runResult;
final RunResult runResult = await _processUtils.run(
pubCommand,
workingDirectory: project.directory.path,
);
// Don't treat this command as terminal if it fails.
// See https://github.com/flutter/flutter/issues/166648
try {
runResult = await _processUtils.run(
pubCommand,
workingDirectory: project.directory.path,
throwOnError: true,
);
} on io.ProcessException catch (e) {
_logger.printWarning('${pubCommand.join(' ')} ${e.message}');
return null;
}
Never fail([String? reason]) {
final String stdout = runResult.stdout;
@ -374,11 +385,6 @@ class _DefaultPub implements Pub {
);
}
// Guard against dart pub deps crashing.
if (runResult.exitCode != 0) {
fail();
}
// Guard against dart pub deps having explicitly invalid output.
try {
final Object? result = json.decode(runResult.stdout);

View File

@ -36,7 +36,6 @@ abstract final class FlutterGlobalOptions {
static const String kMachineFlag = 'machine';
static const String kPackagesOption = 'packages';
static const String kPrefixedErrorsFlag = 'prefixed-errors';
static const String kDtdUrl = 'dtd-url';
static const String kPrintDtd = 'print-dtd';
static const String kQuietFlag = 'quiet';
static const String kShowTestDeviceFlag = 'show-test-device';
@ -152,12 +151,6 @@ class FlutterCommandRunner extends CommandRunner<void> {
hide: !verboseHelp,
help: 'Path to your "package_config.json" file.',
);
argParser.addOption(
FlutterGlobalOptions.kDtdUrl,
help:
'The address of an existing Dart Tooling Daemon instance to be used by the Flutter CLI.',
hide: !verboseHelp,
);
argParser.addFlag(
FlutterGlobalOptions.kPrintDtd,
negatable: false,

View File

@ -1,101 +0,0 @@
// Copyright 2014 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:async';
import 'package:dtd/dtd.dart';
import 'package:process/process.dart';
import '../artifacts.dart';
import '../base/common.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/process.dart';
import '../convert.dart';
/// Provides services, streams, and RPC invocations to interact with the Widget Preview Scaffold.
class WidgetPreviewDtdServices {
WidgetPreviewDtdServices({
required this.logger,
required this.shutdownHooks,
required this.dtdLauncher,
}) {
shutdownHooks.addShutdownHook(() async {
await _dtd?.close();
await dtdLauncher.dispose();
});
}
final Logger logger;
final ShutdownHooks shutdownHooks;
final DtdLauncher dtdLauncher;
DartToolingDaemon? _dtd;
/// The [Uri] pointing to the currently connected DTD instance.
///
/// Returns `null` if there is no DTD connection.
Uri? get dtdUri => _dtdUri;
Uri? _dtdUri;
/// Starts DTD in a child process before invoking [connect] with a [Uri] pointing to the new
/// DTD instance.
Future<void> launchAndConnect() async {
// Connect to the new DTD instance.
await connect(dtdWsUri: await dtdLauncher.launch());
}
/// Connects to an existing DTD instance and registers any relevant services.
Future<void> connect({required Uri dtdWsUri}) async {
_dtdUri = dtdWsUri;
_dtd = await DartToolingDaemon.connect(dtdWsUri);
// TODO(bkonyi): register services.
logger.printTrace('Connected to DTD and registered services.');
}
}
/// Manages the lifecycle of a Dart Tooling Daemon (DTD) instance.
class DtdLauncher {
DtdLauncher({required this.logger, required this.artifacts, required this.processManager});
/// Starts a new DTD instance and returns the web socket URI it's available on.
Future<Uri> launch() async {
if (_dtdProcess != null) {
throw StateError('Attempted to launch DTD twice.');
}
// Start DTD.
_dtdProcess = await processManager.start(<Object>[
artifacts.getArtifactPath(Artifact.engineDartBinary),
'tooling-daemon',
'--machine',
]);
// Wait for the DTD connection information.
final Completer<Uri> dtdUri = Completer<Uri>();
late final StreamSubscription<String> sub;
sub = _dtdProcess!.stdout.transform(const Utf8Decoder()).listen((String data) async {
await sub.cancel();
final Map<String, Object?> jsonData = json.decode(data) as Map<String, Object?>;
if (jsonData case {'tooling_daemon_details': {'uri': final String dtdUriString}}) {
dtdUri.complete(Uri.parse(dtdUriString));
} else {
throwToolExit('Unable to start the Dart Tooling Daemon.');
}
});
return dtdUri.future;
}
/// Kills the spawned DTD instance.
Future<void> dispose() async {
_dtdProcess?.kill();
_dtdProcess = null;
}
final Logger logger;
final Artifacts artifacts;
final ProcessManager processManager;
Process? _dtdProcess;
}

View File

@ -21,7 +21,7 @@ dependencies:
ffi: 2.1.4
file: 7.0.1
flutter_template_images: 5.0.0
html: 0.15.5
html: 0.15.5+1
http: 1.3.0
intl: 0.20.2
meta: 1.16.0
@ -122,4 +122,4 @@ dartdoc:
# Exclude this package from the hosted API docs.
nodoc: true
# PUBSPEC CHECKSUM: fe37
# PUBSPEC CHECKSUM: c093

View File

@ -59,7 +59,7 @@
"required": false
},
"postBuild": {
"description": "The command to be invoked after the build process is done, to do any additional packaging for example.",
"description": "The command to be invoked after the build process is done, to do any additional packaging for example. The following variables are available via string interpolation:\n- ${appName}\n- ${localPath}\n- ${buildMode}\n- ${icuDataPath}\n- ${engineRevision}",
"type": ["array", "null"],
"items": {
"type": "string"
@ -91,7 +91,7 @@
]
},
"runDebug": {
"description": "The command to be invoked to run the app in debug mode. The name of the app to be started is available via the ${appName} string interpolation. Make sure the flutter cmdline output is available via this commands stdout/stderr since the SDK needs the \"VM Service is now listening on ...\" message to function. If the forwardPort command is not specified, the VM Service URL will be connected to as-is, without any port forwarding. In that case you need to make sure it is reachable from your host device, possibly via the \"--vm-service-host=<ip>\" engine flag.",
"description": "The command to be invoked to run the app in debug mode. Make sure the flutter cmdline output is available via this commands stdout/stderr since the SDK needs the \"VM Service is now listening on ...\" message to function. If the forwardPort command is not specified, the VM Service URL will be connected to as-is, without any port forwarding. In that case you need to make sure it is reachable from your host device, possibly via the \"--vm-service-host=<ip>\" engine flag. The following variables are available via string interpolation:\n- ${appName}\n- ${engineOptions}\n- ${buildMode}\n- ${icuDataPath}\n- ${engineRevision}",
"type": "array",
"items": {
"type": "string"

View File

@ -356,7 +356,6 @@
"templates/widget_preview_scaffold/lib/src/widget_preview.dart.tmpl",
"templates/widget_preview_scaffold/lib/src/widget_preview_rendering.dart.tmpl",
"templates/widget_preview_scaffold/lib/src/controls.dart.tmpl",
"templates/widget_preview_scaffold/lib/src/dtd_services.dart.tmpl",
"templates/widget_preview_scaffold/lib/src/generated_preview.dart.tmpl",
"templates/widget_preview_scaffold/lib/src/utils.dart.tmpl",
"templates/widget_preview_scaffold/pubspec.yaml.tmpl",

View File

@ -1,33 +0,0 @@
// Copyright 2014 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:async';
import 'package:dtd/dtd.dart';
/// Provides services, streams, and RPC invocations to interact with Flutter developer tooling.
class WidgetPreviewScaffoldDtdServices {
/// Environment variable for the DTD URI.
static const String kWidgetPreviewDtdUriEnvVar = 'WIDGET_PREVIEW_DTD_URI';
/// Connects to the Dart Tooling Daemon (DTD) specified by the Flutter tool.
///
/// If the connection is successful, the Widget Preview Scaffold will register services and
/// subscribe to various streams to interact directly with other tooling (e.g., IDEs).
Future<void> connect() async {
final Uri dtdWsUri = Uri.parse(
const String.fromEnvironment(kWidgetPreviewDtdUriEnvVar),
);
_dtd = await DartToolingDaemon.connect(dtdWsUri);
unawaited(
_dtd.postEvent(
'WidgetPreviewScaffold',
'Connected',
const <String, Object?>{},
),
);
}
late final DartToolingDaemon _dtd;
}

View File

@ -13,7 +13,6 @@ import 'package:flutter/services.dart';
import 'package:stack_trace/stack_trace.dart';
import 'controls.dart';
import 'dtd_services.dart';
import 'generated_preview.dart';
import 'utils.dart';
import 'widget_preview.dart';
@ -411,8 +410,6 @@ class PreviewAssetBundle extends PlatformAssetBundle {
/// the preview scaffold project which prevents us from being able to use hot
/// restart to iterate on this file.
Future<void> mainImpl() async {
// TODO(bkonyi): store somewhere.
await WidgetPreviewScaffoldDtdServices().connect();
runApp(_WidgetPreviewScaffold());
}

View File

@ -12,7 +12,6 @@ dependencies:
flutter_test:
sdk: flutter
# These will be replaced with proper constraints after the template is hydrated.
dtd: any
flutter_lints: any
stack_trace: any

View File

@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'package:file/memory.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart';
@ -48,8 +47,6 @@ void main() {
platform: platform,
processManager: processManager,
),
processManager: FakeProcessManager.any(),
artifacts: Artifacts.test(fileSystem: fileSystem),
);
rootProject = FakeFlutterProject(
projectRoot: 'some_project',

View File

@ -6,7 +6,6 @@ import 'dart:io' as io show IOOverrides;
import 'package:args/command_runner.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/bot_detector.dart';
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/file_system.dart';
@ -81,8 +80,6 @@ void main() {
logger: logger,
platform: platform,
),
artifacts: Artifacts.test(),
processManager: FakeProcessManager.any(),
),
);
await runner.run(<String>['widget-preview', ...arguments]);

View File

@ -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 'dart:io' as io;
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/logger.dart';
@ -356,6 +358,17 @@ void main() {
);
});
test('a pub error is treated as no data available instead of terminal', () async {
final ProcessManager processes = _dartPubDepsCrashes(project: project);
final Set<String> dependencies = await computeExclusiveDevDependencies(
pub(processes),
project: project,
logger: logger,
);
expect(dependencies, isEmpty, reason: 'pub deps crashed, but was not terminal');
});
test('throws and logs on invalid JSON', () async {
final ProcessManager processes = _dartPubDepsReturns('''
{
@ -420,3 +433,18 @@ ProcessManager _dartPubDepsReturns(String dartPubDepsOutput, {required FlutterPr
),
]);
}
ProcessManager _dartPubDepsCrashes({required FlutterProject project}) {
return FakeProcessManager.list(<FakeCommand>[
FakeCommand(
command: const <String>[_dartBin, 'pub', '--suppress-analytics', 'deps', '--json'],
workingDirectory: project.directory.path,
exception: const io.ProcessException('pub', <String>[
'pub',
'--suppress-analytics',
'deps',
'--json',
]),
),
]);
}

View File

@ -7,6 +7,7 @@ import 'dart:async';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
@ -530,6 +531,100 @@ void main() {
},
);
testUsingContext(
'custom device command string interpolation end-to-end test',
() async {
final Completer<void> runDebugCompleter = Completer<void>();
final CustomDeviceConfig config = testConfig.copyWith(
platform: TargetPlatform.linux_arm64,
postBuildCommand: const <String>[
'testpostbuild',
r'--buildMode=${buildMode}',
r'--icuDataPath=${icuDataPath}',
r'--engineRevision=${engineRevision}',
],
runDebugCommand: const <String>[
'testrundebug',
r'--buildMode=${buildMode}',
r'--icuDataPath=${icuDataPath}',
r'--engineRevision=${engineRevision}',
],
);
final List<Pattern> commandArgumentsPattern = <Pattern>[
RegExp(r'--buildMode=.*'),
RegExp(r'--icuDataPath=.*'),
RegExp(r'--engineRevision=.*'),
];
final String expectedIcuDataPath = globals.artifacts!.getArtifactPath(
Artifact.icuData,
platform: config.platform,
);
final String expectedEngineRevision = globals.flutterVersion.engineRevision;
final List<String> expectedCommandArguments = <String>[
'--buildMode=debug',
'--icuDataPath=$expectedIcuDataPath',
'--engineRevision=$expectedEngineRevision',
];
final List<String> expectedRunDebugCommand = <String>[
'testrundebug',
...expectedCommandArguments,
];
final List<String> expectedPostBuildCommand = <String>[
'testpostbuild',
...expectedCommandArguments,
];
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
FakeCommand(
command: <Pattern>['testpostbuild', ...commandArgumentsPattern],
onRun: (List<String> command) => expect(command, expectedPostBuildCommand),
),
FakeCommand(command: config.uninstallCommand),
FakeCommand(command: config.installCommand),
FakeCommand(
command: <Pattern>['testrundebug', ...commandArgumentsPattern],
completer: runDebugCompleter,
onRun: (List<String> command) => expect(command, expectedRunDebugCommand),
stdout: 'The Dart VM service is listening on http://127.0.0.1:12345/abcd/\n',
),
FakeCommand(
command: config.forwardPortCommand!,
stdout: testConfigForwardPortSuccessOutput,
),
]);
// CustomDevice.startApp doesn't care whether we pass a prebuilt app or
// buildable app as long as we pass prebuiltApplication as false
final PrebuiltLinuxApp app = PrebuiltLinuxApp(executable: 'testexecutable');
// finally start actually testing things
final CustomDevice device = CustomDevice(
config: config,
logger: BufferLogger.test(),
processManager: processManager,
);
await device.startApp(
app,
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
bundleBuilder: FakeBundleBuilder(),
);
expect(runDebugCompleter.isCompleted, false);
expect(await device.stopApp(app), true);
expect(runDebugCompleter.isCompleted, true);
},
overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
},
);
testWithoutContext('CustomDevice screenshotting', () async {
bool screenshotCommandWasExecuted = false;

View File

@ -43,7 +43,7 @@ void main() {
);
});
testWithoutContext('fails on non-zero exit code', () async {
testWithoutContext('returns null on non-zero exit code', () async {
final BufferLogger logger = BufferLogger.test();
final MemoryFileSystem fileSystem = MemoryFileSystem.test();
final ProcessManager processManager = _dartPubDepsFails(
@ -62,14 +62,8 @@ void main() {
);
await expectLater(
() => pub.deps(FlutterProject.fromDirectoryTest(fileSystem.currentDirectory)),
throwsA(
isA<StateError>().having(
(StateError e) => e.message,
'message',
contains('dart pub --suppress-analytics deps --json failed'),
),
),
pub.deps(FlutterProject.fromDirectoryTest(fileSystem.currentDirectory)),
completion(isNull),
);
});

View File

@ -5,18 +5,12 @@
import 'dart:async';
import 'dart:convert';
import 'package:dtd/dtd.dart';
import 'package:file/file.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/commands/widget_preview.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/runner/flutter_command_runner.dart';
import 'package:flutter_tools/src/widget_preview/dtd_services.dart';
import 'package:process/process.dart';
import '../src/common.dart';
import '../src/context.dart';
import 'test_data/basic_project.dart';
import 'test_utils.dart';
@ -49,13 +43,10 @@ const List<String> subsequentLaunchMessagesWeb = <String>[
void main() {
late Directory tempDir;
Process? process;
Logger? logger;
DtdLauncher? dtdLauncher;
final BasicProject project = BasicProject();
const ProcessManager processManager = LocalProcessManager();
setUp(() async {
logger = BufferLogger.test();
tempDir = createResolvedTempDirectorySync('widget_preview_test.');
await project.setUpIn(tempDir);
});
@ -63,15 +54,12 @@ void main() {
tearDown(() async {
process?.kill();
process = null;
await dtdLauncher?.dispose();
dtdLauncher = null;
tryToDelete(tempDir);
});
Future<void> runWidgetPreview({
required List<String> expectedMessages,
bool useWeb = false,
Uri? dtdUri,
}) async {
expect(expectedMessages, isNotEmpty);
int i = 0;
@ -84,7 +72,6 @@ void main() {
'--${WidgetPreviewStartCommand.kHeadlessWeb}'
else
'--${WidgetPreviewStartCommand.kUseFlutterDesktop}',
if (dtdUri != null) '--${FlutterGlobalOptions.kDtdUrl}=$dtdUri',
], workingDirectory: tempDir.path);
final Completer<void> completer = Completer<void>();
@ -116,6 +103,8 @@ void main() {
}),
);
await completer.future;
process!.kill();
process = null;
}
group('flutter widget-preview start', () {
@ -143,38 +132,5 @@ void main() {
// We shouldn't regenerate the scaffold after the initial run.
await runWidgetPreview(expectedMessages: subsequentLaunchMessagesWeb, useWeb: true);
});
testUsingContext('can connect to an existing DTD instance', () async {
dtdLauncher = DtdLauncher(
logger: logger!,
artifacts: globals.artifacts!,
processManager: globals.processManager,
);
// Start a DTD instance.
final Uri dtdUri = await dtdLauncher!.launch();
// Connect to it and listen to the WidgetPreviewScaffold stream.
//
// The preview scaffold will send a 'Connected' event on this stream once it has initialized
// and is ready.
final DartToolingDaemon dtdConnection = await DartToolingDaemon.connect(dtdUri);
const String kWidgetPreviewScaffoldStream = 'WidgetPreviewScaffold';
final Completer<void> completer = Completer<void>();
dtdConnection.onEvent(kWidgetPreviewScaffoldStream).listen((DTDEvent event) {
expect(event.stream, kWidgetPreviewScaffoldStream);
expect(event.kind, 'Connected');
completer.complete();
});
await dtdConnection.streamListen(kWidgetPreviewScaffoldStream);
// Start the widget preview and wait for the 'Connected' event.
await runWidgetPreview(
expectedMessages: firstLaunchMessagesWeb,
useWeb: true,
dtdUri: dtdUri,
);
await completer.future;
});
});
}

View File

@ -1,33 +0,0 @@
// Copyright 2014 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:async';
import 'package:dtd/dtd.dart';
/// Provides services, streams, and RPC invocations to interact with Flutter developer tooling.
class WidgetPreviewScaffoldDtdServices {
/// Environment variable for the DTD URI.
static const String kWidgetPreviewDtdUriEnvVar = 'WIDGET_PREVIEW_DTD_URI';
/// Connects to the Dart Tooling Daemon (DTD) specified by the Flutter tool.
///
/// If the connection is successful, the Widget Preview Scaffold will register services and
/// subscribe to various streams to interact directly with other tooling (e.g., IDEs).
Future<void> connect() async {
final Uri dtdWsUri = Uri.parse(
const String.fromEnvironment(kWidgetPreviewDtdUriEnvVar),
);
_dtd = await DartToolingDaemon.connect(dtdWsUri);
unawaited(
_dtd.postEvent(
'WidgetPreviewScaffold',
'Connected',
const <String, Object?>{},
),
);
}
late final DartToolingDaemon _dtd;
}

View File

@ -13,7 +13,6 @@ import 'package:flutter/services.dart';
import 'package:stack_trace/stack_trace.dart';
import 'controls.dart';
import 'dtd_services.dart';
import 'generated_preview.dart';
import 'utils.dart';
import 'widget_preview.dart';
@ -411,8 +410,6 @@ class PreviewAssetBundle extends PlatformAssetBundle {
/// the preview scaffold project which prevents us from being able to use hot
/// restart to iterate on this file.
Future<void> mainImpl() async {
// TODO(bkonyi): store somewhere.
await WidgetPreviewScaffoldDtdServices().connect();
runApp(_WidgetPreviewScaffold());
}

View File

@ -12,7 +12,6 @@ dependencies:
flutter_test:
sdk: flutter
# These will be replaced with proper constraints after the template is hydrated.
dtd: 2.5.0
flutter_lints: 5.0.0
stack_trace: 1.12.1
@ -21,13 +20,7 @@ dependencies:
characters: 1.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
clock: 1.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
collection: 1.19.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
convert: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
crypto: 3.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
fake_async: 1.3.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
file: 7.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
http: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
http_parser: 4.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
json_rpc_2: 3.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
leak_tracker: 10.0.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
leak_tracker_flutter_testing: 3.0.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
leak_tracker_testing: 3.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
@ -41,15 +34,10 @@ dependencies:
string_scanner: 1.4.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
term_glyph: 1.2.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
test_api: 0.7.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
typed_data: 1.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
unified_analytics: 7.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
vm_service: 15.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
web: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
web_socket: 0.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
web_socket_channel: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
flutter:
uses-material-design: true
# PUBSPEC CHECKSUM: 4b12
# PUBSPEC CHECKSUM: 367e