Semantics

This commit is contained in:
Hixie 2015-12-16 11:11:18 -08:00
parent b5470df8d7
commit 28a1788371
56 changed files with 3395 additions and 181 deletions

View file

@ -2,49 +2,16 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
class PageSelectorDemo extends StatelessComponent {
Widget _buildTabIndicator(BuildContext context, String iconName) {
final Color color = Theme.of(context).primaryColor;
final ColorTween _selectedColor = new ColorTween(begin: Colors.transparent, end: color);
final ColorTween _previousColor = new ColorTween(begin: color, end: Colors.transparent);
final TabBarSelectionState selection = TabBarSelection.of(context);
CurvedAnimation animation = new CurvedAnimation(parent: selection.animation, curve: Curves.ease);
return new AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget child) {
Color background = selection.value == iconName ? _selectedColor.end : _selectedColor.begin;
if (selection.valueIsChanging) {
// Then the selection's animation is animating from previousValue to value.
if (selection.value == iconName)
background = _selectedColor.evaluate(animation);
else if (selection.previousValue == iconName)
background = _previousColor.evaluate(animation);
}
return new Container(
width: 12.0,
height: 12.0,
margin: new EdgeDims.all(4.0),
decoration: new BoxDecoration(
backgroundColor: background,
border: new Border.all(color: _selectedColor.end),
shape: BoxShape.circle
)
);
}
);
}
Widget _buildTabView(String iconName) {
return new Container(
key: new ValueKey<String>(iconName),
padding: const EdgeDims.all(12.0),
child: new Card(
child: new Center(
child: new Icon(icon: "action/$iconName", size:IconSize.s48)
child: new Icon(icon: 'action/$iconName', size:IconSize.s48)
)
)
);
@ -57,10 +24,10 @@ class PageSelectorDemo extends StatelessComponent {
}
Widget build(BuildContext notUsed) { // Can't find the TabBarSelection from this context.
final List<String> iconNames = <String>["event", "home", "android", "alarm", "face", "language"];
final List<String> iconNames = <String>['event', 'home', 'android', 'alarm', 'face', 'language'];
return new Scaffold(
toolBar: new ToolBar(center: new Text("Page Selector")),
toolBar: new ToolBar(center: new Text('Page Selector')),
body: new TabBarSelection(
values: iconNames,
child: new Builder(
@ -72,16 +39,13 @@ class PageSelectorDemo extends StatelessComponent {
child: new Row(
children: <Widget>[
new IconButton(
icon: "navigation/arrow_back",
icon: 'navigation/arrow_back',
onPressed: () { _handleArrowButtonPress(context, -1); },
tooltip: 'Back'
),
new Row(
children: iconNames.map((String name) => _buildTabIndicator(context, name)).toList(),
justifyContent: FlexJustifyContent.collapse
),
new TabPageSelector<String>(),
new IconButton(
icon: "navigation/arrow_forward",
icon: 'navigation/arrow_forward',
onPressed: () { _handleArrowButtonPress(context, 1); },
tooltip: 'Forward'
)

View file

@ -103,6 +103,7 @@ abstract class RenderSector extends RenderObject {
}
Rect get paintBounds => new Rect.fromLTWH(0.0, 0.0, 2.0 * deltaRadius, 2.0 * deltaRadius);
Rect get semanticBounds => new Rect.fromLTWH(-deltaRadius, -deltaRadius, 2.0 * deltaRadius, 2.0 * deltaRadius);
bool hitTest(HitTestResult result, { double radius, double theta }) {
if (radius < parentData.radius || radius >= parentData.radius + deltaRadius ||

View file

@ -2,6 +2,7 @@ name: stocks
version: 0.0.2
update-url: http://localhost:9888/
material-design-icons:
- name: action/accessibility
- name: action/account_balance
- name: action/assessment
- name: action/backup

View file

@ -43,7 +43,8 @@ class StocksAppState extends State<StocksApp> {
backupMode: BackupMode.enabled,
debugShowGrid: false,
debugShowSizes: false,
showPerformanceOverlay: false
showPerformanceOverlay: false,
showSemanticsDebugger: false
);
void initState() {
@ -110,6 +111,7 @@ class StocksAppState extends State<StocksApp> {
theme: theme,
debugShowMaterialGrid: _configuration.debugShowGrid,
showPerformanceOverlay: _configuration.showPerformanceOverlay,
showSemanticsDebugger: _configuration.showSemanticsDebugger,
routes: <String, RouteBuilder>{
'/': (RouteArguments args) => new StockHome(_stocks, _symbols, _configuration, configurationUpdater),
'/settings': (RouteArguments args) => new StockSettings(_configuration, configurationUpdater)

View file

@ -88,16 +88,16 @@ class StockHomeState extends State<StockHome> {
content: new Text('This feature has not yet been implemented.'),
actions: <Widget>[
new FlatButton(
child: new Text('USE IT'),
onPressed: () {
Navigator.pop(context, false);
}
},
child: new Text('USE IT')
),
new FlatButton(
child: new Text('OH WELL'),
onPressed: () {
Navigator.pop(context, false);
}
},
child: new Text('OH WELL')
),
]
)
@ -107,7 +107,16 @@ class StockHomeState extends State<StockHome> {
),
new DrawerItem(
icon: 'device/dvr',
onPressed: () { debugDumpApp(); debugDumpRenderTree(); debugDumpLayerTree(); },
onPressed: () {
try {
debugDumpApp();
debugDumpRenderTree();
debugDumpLayerTree();
debugDumpSemanticsTree();
} catch (e, stack) {
debugPrint('Exception while dumping app:\n$e\n$stack');
}
},
child: new Text('Dump App to Console')
),
new DrawerDivider(),

View file

@ -35,6 +35,10 @@ class StockSettingsState extends State<StockSettings> {
sendUpdates(config.configuration.copyWith(showPerformanceOverlay: value));
}
void _handleShowSemanticsDebuggerChanged(bool value) {
sendUpdates(config.configuration.copyWith(showSemanticsDebugger: value));
}
void _confirmOptimismChange() {
switch (config.configuration.stockMode) {
case StockMode.optimistic:
@ -118,6 +122,19 @@ class StockSettingsState extends State<StockSettings> {
]
)
),
new DrawerItem(
icon: 'action/accessibility',
onPressed: () { _handleShowSemanticsDebuggerChanged(!config.configuration.showSemanticsDebugger); },
child: new Row(
children: <Widget>[
new Flexible(child: new Text('Show semantics overlay')),
new Switch(
value: config.configuration.showSemanticsDebugger,
onChanged: _handleShowSemanticsDebuggerChanged
),
]
)
),
];
assert(() {
// material grid and size construction lines are only available in checked mode

View file

@ -13,13 +13,15 @@ class StockConfiguration {
this.backupMode,
this.debugShowGrid,
this.debugShowSizes,
this.showPerformanceOverlay
this.showPerformanceOverlay,
this.showSemanticsDebugger
}) {
assert(stockMode != null);
assert(backupMode != null);
assert(debugShowGrid != null);
assert(debugShowSizes != null);
assert(showPerformanceOverlay != null);
assert(showSemanticsDebugger != null);
}
final StockMode stockMode;
@ -27,20 +29,23 @@ class StockConfiguration {
final bool debugShowGrid;
final bool debugShowSizes;
final bool showPerformanceOverlay;
final bool showSemanticsDebugger;
StockConfiguration copyWith({
StockMode stockMode,
BackupMode backupMode,
bool debugShowGrid,
bool debugShowSizes,
bool showPerformanceOverlay
bool showPerformanceOverlay,
bool showSemanticsDebugger
}) {
return new StockConfiguration(
stockMode: stockMode ?? this.stockMode,
backupMode: backupMode ?? this.backupMode,
debugShowGrid: debugShowGrid ?? this.debugShowGrid,
debugShowSizes: debugShowSizes ?? this.debugShowSizes,
showPerformanceOverlay: showPerformanceOverlay ?? this.showPerformanceOverlay
showPerformanceOverlay: showPerformanceOverlay ?? this.showPerformanceOverlay,
showSemanticsDebugger: showSemanticsDebugger ?? this.showSemanticsDebugger
);
}
}
}

View file

@ -25,6 +25,7 @@ export 'src/rendering/overflow.dart';
export 'src/rendering/paragraph.dart';
export 'src/rendering/performance_overlay.dart';
export 'src/rendering/proxy_box.dart';
export 'src/rendering/semantics.dart';
export 'src/rendering/shifted_box.dart';
export 'src/rendering/stack.dart';
export 'src/rendering/view.dart';

View file

@ -31,7 +31,7 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
);
GestureTapDownCallback onTapDown;
GestureTapDownCallback onTapUp;
GestureTapUpCallback onTapUp;
GestureTapCallback onTap;
GestureTapCancelCallback onTapCancel;

View file

@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'debug.dart';
import 'icon.dart';
import 'tooltip.dart';
const double _kChipHeight = 32.0;
const double _kAvatarDiamater = _kChipHeight;
@ -41,11 +42,13 @@ class Chip extends StatelessComponent {
if (avatar != null) {
leftPadding = 0.0;
children.add(new Container(
margin: const EdgeDims.only(right: 8.0),
width: _kAvatarDiamater,
height: _kAvatarDiamater,
child: avatar
children.add(new ExcludeSemantics(
child: new Container(
margin: const EdgeDims.only(right: 8.0),
width: _kAvatarDiamater,
height: _kAvatarDiamater,
child: avatar
)
));
}
@ -58,27 +61,33 @@ class Chip extends StatelessComponent {
rightPadding = 0.0;
children.add(new GestureDetector(
onTap: onDeleted,
child: new Container(
padding: const EdgeDims.symmetric(horizontal: 4.0),
child: new Icon(
icon: 'navigation/cancel',
size: IconSize.s18,
color: Colors.black54
child: new Tooltip(
message: 'Delete "$label"',
child: new Container(
padding: const EdgeDims.symmetric(horizontal: 4.0),
child: new Icon(
icon: 'navigation/cancel',
size: IconSize.s18,
color: Colors.black54
)
)
)
));
}
return new Container(
height: _kChipHeight,
padding: new EdgeDims.only(left: leftPadding, right: rightPadding),
decoration: new BoxDecoration(
backgroundColor: Colors.grey[300],
borderRadius: 16.0
),
child: new Row(
children: children,
justifyContent: FlexJustifyContent.collapse
return new Semantics(
container: true,
child: new Container(
height: _kChipHeight,
padding: new EdgeDims.only(left: leftPadding, right: rightPadding),
decoration: new BoxDecoration(
backgroundColor: Colors.grey[300],
borderRadius: 16.0
),
child: new Row(
children: children,
justifyContent: FlexJustifyContent.collapse
)
)
);
}

View file

@ -125,7 +125,7 @@ class DrawerControllerState extends State<DrawerController> {
});
}
void _handlePointerDown(_) {
void _handleTapDown(Point position) {
_controller.stop();
_ensureHistoryEntry();
}
@ -166,6 +166,7 @@ class DrawerControllerState extends State<DrawerController> {
onHorizontalDragUpdate: _move,
onHorizontalDragEnd: _settle,
behavior: HitTestBehavior.translucent,
excludeFromSemantics: true,
child: new Container(width: _kEdgeDragWidth)
)
);
@ -188,8 +189,8 @@ class DrawerControllerState extends State<DrawerController> {
),
new Align(
alignment: const FractionalOffset(0.0, 0.5),
child: new Listener(
onPointerDown: _handlePointerDown,
child: new GestureDetector(
onTapDown: _handleTapDown,
child: new Align(
alignment: const FractionalOffset(1.0, 0.5),
widthFactor: _controller.value,

View file

@ -70,11 +70,13 @@ class DrawerItem extends StatelessComponent {
)
);
return new Container(
height: 48.0,
child: new InkWell(
onTap: onPressed,
child: new Row(children: children)
return new MergeSemantics(
child: new Container(
height: 48.0,
child: new InkWell(
onTap: onPressed,
child: new Row(children: children)
)
)
);
}

View file

@ -26,15 +26,12 @@ class IconButton extends StatelessComponent {
final String tooltip;
Widget build(BuildContext context) {
Widget result = new InkResponse(
onTap: onPressed,
child: new Padding(
padding: const EdgeDims.all(8.0),
child: new Icon(
icon: icon,
colorTheme: colorTheme,
color: color
)
Widget result = new Padding(
padding: const EdgeDims.all(8.0),
child: new Icon(
icon: icon,
colorTheme: colorTheme,
color: color
)
);
if (tooltip != null) {
@ -43,7 +40,10 @@ class IconButton extends StatelessComponent {
child: result
);
}
return result;
return new InkResponse(
onTap: onPressed,
child: result
);
}
void debugFillDescription(List<String> description) {

View file

@ -49,12 +49,14 @@ class MaterialApp extends StatefulComponent {
this.onGenerateRoute,
this.onLocaleChanged,
this.debugShowMaterialGrid: false,
this.showPerformanceOverlay: false
this.showPerformanceOverlay: false,
this.showSemanticsDebugger: false
}) : super(key: key) {
assert(routes != null);
assert(routes.containsKey(Navigator.defaultRouteName) || onGenerateRoute != null);
assert(debugShowMaterialGrid != null);
assert(showPerformanceOverlay != null);
assert(showSemanticsDebugger != null);
}
final String title;
@ -64,6 +66,7 @@ class MaterialApp extends StatefulComponent {
final LocaleChangedCallback onLocaleChanged;
final bool debugShowMaterialGrid;
final bool showPerformanceOverlay;
final bool showSemanticsDebugger;
_MaterialAppState createState() => new _MaterialAppState();
}
@ -194,6 +197,11 @@ class _MaterialAppState extends State<MaterialApp> implements BindingObserver {
]
);
}
if (config.showSemanticsDebugger) {
result = new SemanticsDebugger(
child: result
);
}
return result;
}

View file

@ -32,14 +32,16 @@ class PopupMenuItem<T> extends StatelessComponent {
final T value;
Widget build(BuildContext context) {
return new Container(
height: _kMenuItemHeight,
padding: const EdgeDims.symmetric(horizontal: _kMenuHorizontalPadding),
child: new DefaultTextStyle(
style: Theme.of(context).text.subhead,
child: new Baseline(
baseline: _kMenuItemHeight - _kBaselineOffsetFromBottom,
child: child
return new MergeSemantics(
child: new Container(
height: _kMenuItemHeight,
padding: const EdgeDims.symmetric(horizontal: _kMenuHorizontalPadding),
child: new DefaultTextStyle(
style: Theme.of(context).text.subhead,
child: new Baseline(
baseline: _kMenuItemHeight - _kBaselineOffsetFromBottom,
child: child
)
)
)
);

View file

@ -44,11 +44,14 @@ class Radio<T> extends StatelessComponent {
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
ThemeData themeData = Theme.of(context);
return new _RadioRenderObjectWidget(
selected: value == groupValue,
activeColor: activeColor ?? themeData.accentColor,
inactiveColor: _getInactiveColor(themeData),
onChanged: _enabled ? _handleChanged : null
return new Semantics(
checked: value == groupValue,
child: new _RadioRenderObjectWidget(
selected: value == groupValue,
activeColor: activeColor ?? themeData.accentColor,
inactiveColor: _getInactiveColor(themeData),
onChanged: _enabled ? _handleChanged : null
)
);
}
}

View file

@ -495,10 +495,13 @@ class _PersistentBottomSheetState extends State<_PersistentBottomSheet> {
child: child
);
},
child: new BottomSheet(
animationController: config.animationController,
onClosing: config.onClosing,
builder: config.builder
child: new Semantics(
container: true,
child: new BottomSheet(
animationController: config.animationController,
onClosing: config.onClosing,
builder: config.builder
)
)
);
}

View file

@ -95,23 +95,26 @@ class SnackBar extends StatelessComponent {
child: child
);
},
child: new Material(
elevation: 6,
color: _kSnackBackground,
child: new Container(
margin: const EdgeDims.symmetric(horizontal: _kSideMargins),
child: new Theme(
data: new ThemeData(
brightness: ThemeBrightness.dark,
accentColor: theme.accentColor,
accentColorBrightness: theme.accentColorBrightness,
text: Typography.white
),
child: new FadeTransition(
opacity: fadeAnimation,
child: new Row(
children: children,
alignItems: FlexAlignItems.center
child: new Semantics(
container: true,
child: new Material(
elevation: 6,
color: _kSnackBackground,
child: new Container(
margin: const EdgeDims.symmetric(horizontal: _kSideMargins),
child: new Theme(
data: new ThemeData(
brightness: ThemeBrightness.dark,
accentColor: theme.accentColor,
accentColorBrightness: theme.accentColorBrightness,
text: Typography.white
),
child: new FadeTransition(
opacity: fadeAnimation,
child: new Row(
children: children,
alignItems: FlexAlignItems.center
)
)
)
)

View file

@ -405,7 +405,7 @@ class TabBarSelection<T> extends StatefulComponent {
final ValueChanged<T> onChanged;
final Widget child;
TabBarSelectionState createState() => new TabBarSelectionState<T>();
TabBarSelectionState<T> createState() => new TabBarSelectionState<T>();
static TabBarSelectionState of(BuildContext context) {
return context.ancestorStateOfType(const TypeMatcher<TabBarSelectionState>());
@ -952,3 +952,52 @@ class _TabBarViewState extends PageableListState<TabBarView> implements TabBarSe
);
}
}
class TabPageSelector<T> extends StatelessComponent {
const TabPageSelector({ Key key }) : super(key: key);
Widget _buildTabIndicator(TabBarSelectionState<T> selection, T tab, Animation animation, ColorTween selectedColor, ColorTween previousColor) {
Color background;
if (selection.valueIsChanging) {
// The selection's animation is animating from previousValue to value.
if (selection.value == tab)
background = selectedColor.evaluate(animation);
else if (selection.previousValue == tab)
background = previousColor.evaluate(animation);
else
background = selectedColor.begin;
} else {
background = selection.value == tab ? selectedColor.end : selectedColor.begin;
}
return new Container(
width: 12.0,
height: 12.0,
margin: new EdgeDims.all(4.0),
decoration: new BoxDecoration(
backgroundColor: background,
border: new Border.all(color: selectedColor.end),
shape: BoxShape.circle
)
);
}
Widget build(BuildContext context) {
final TabBarSelectionState selection = TabBarSelection.of(context);
final Color color = Theme.of(context).primaryColor;
final ColorTween selectedColor = new ColorTween(begin: Colors.transparent, end: color);
final ColorTween previousColor = new ColorTween(begin: color, end: Colors.transparent);
Animation<double> animation = new CurvedAnimation(parent: selection.animation, curve: Curves.ease);
return new AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget child) {
return new Semantics(
label: 'Page ${selection.index + 1} of ${selection.values.length}',
child: new Row(
children: selection.values.map((T tab) => _buildTabIndicator(selection, tab, animation, selectedColor, previousColor)).toList(),
justifyContent: FlexJustifyContent.collapse
)
);
}
);
}
}

View file

@ -14,17 +14,18 @@ const Duration _kToggleDuration = const Duration(milliseconds: 200);
// toggle animations. It handles storing the current value, dispatching
// ValueChanged on a tap gesture and driving a changed animation. Subclasses are
// responsible for painting.
abstract class RenderToggleable extends RenderConstrainedBox {
abstract class RenderToggleable extends RenderConstrainedBox implements SemanticActionHandler {
RenderToggleable({
bool value,
Size size,
Color activeColor,
Color inactiveColor,
this.onChanged,
ValueChanged<bool> onChanged,
double minRadialReactionRadius: 0.0
}) : _value = value,
_activeColor = activeColor,
_inactiveColor = inactiveColor,
_onChanged = onChanged,
super(additionalConstraints: new BoxConstraints.tight(size)) {
assert(value != null);
assert(activeColor != null);
@ -61,6 +62,7 @@ abstract class RenderToggleable extends RenderConstrainedBox {
if (value == _value)
return;
_value = value;
markNeedsSemanticsUpdate(onlyChanges: true, noGeometry: true);
_position
..curve = Curves.easeIn
..reverseCurve = Curves.easeOut;
@ -87,9 +89,20 @@ abstract class RenderToggleable extends RenderConstrainedBox {
markNeedsPaint();
}
bool get isInteractive => onChanged != null;
ValueChanged<bool> get onChanged => _onChanged;
ValueChanged<bool> _onChanged;
void set onChanged(ValueChanged<bool> value) {
if (value == _onChanged)
return;
final bool wasInteractive = isInteractive;
_onChanged = value;
if (wasInteractive != isInteractive) {
markNeedsPaint();
markNeedsSemanticsUpdate(noGeometry: true);
}
}
ValueChanged<bool> onChanged;
bool get isInteractive => onChanged != null;
CurvedAnimation get position => _position;
CurvedAnimation _position;
@ -146,4 +159,20 @@ abstract class RenderToggleable extends RenderConstrainedBox {
canvas.drawCircle(offset.toPoint(), _reaction.value, reactionPaint);
}
}
bool get hasSemantics => isInteractive;
Iterable<SemanticAnnotator> getSemanticAnnotators() sync* {
yield (SemanticsNode semantics) {
semantics.hasCheckedState = true;
semantics.isChecked = _value;
semantics.canBeTapped = isInteractive;
};
}
void handleSemanticTap() => _handleTap();
void handleSemanticLongPress() { }
void handleSemanticScrollLeft() { }
void handleSemanticScrollRight() { }
void handleSemanticScrollUp() { }
void handleSemanticScrollDown() { }
}

View file

@ -67,6 +67,13 @@ class Tooltip extends StatefulComponent {
final Widget child;
_TooltipState createState() => new _TooltipState();
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('"$message"');
description.add('vertical offset: $verticalOffset');
description.add('position: ${preferBelow ? "below" : "above"}');
}
}
class _TooltipState extends State<Tooltip> {
@ -175,7 +182,11 @@ class _TooltipState extends State<Tooltip> {
return new GestureDetector(
behavior: HitTestBehavior.opaque,
onLongPress: showTooltip,
child: config.child
excludeFromSemantics: true,
child: new Semantics(
label: config.message,
child: config.child
)
);
}
}

View file

@ -11,9 +11,10 @@ import 'text_style.dart';
abstract class TextSpan {
// This class must be immutable, because we won't notice when it changes.
const TextSpan();
String toString([String prefix = '']);
void build(ui.ParagraphBuilder builder);
ui.ParagraphStyle get paragraphStyle => null;
String toPlainText(); // for semantics
String toString([String prefix = '']); // for debugging
}
/// An immutable span of unstyled text.
@ -37,6 +38,7 @@ class PlainTextSpan extends TextSpan {
int get hashCode => text.hashCode;
String toPlainText() => text;
String toString([String prefix = '']) => '$prefix$runtimeType: "$text"';
}
@ -81,6 +83,8 @@ class StyledTextSpan extends TextSpan {
int get hashCode => hashValues(style, hashList(children));
String toPlainText() => children.map((TextSpan child) => child.toPlainText()).join();
String toString([String prefix = '']) {
List<String> result = <String>[];
result.add('$prefix$runtimeType:');
@ -94,7 +98,7 @@ class StyledTextSpan extends TextSpan {
/// An object that paints a [TextSpan] into a canvas.
class TextPainter {
TextPainter(TextSpan text) {
TextPainter([ TextSpan text ]) {
this.text = text;
}

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:vector_math/vector_math_64.dart';
@ -16,6 +17,7 @@ class MatrixUtils {
///
/// Returns null, otherwise.
static Offset getAsTranslation(Matrix4 transform) {
assert(transform != null);
Float64List values = transform.storage;
// Values are stored in column-major order.
if (values[0] == 1.0 &&
@ -37,4 +39,84 @@ class MatrixUtils {
return null;
}
/// Returns true if the given matrices are exactly equal, and false
/// otherwise. Null values are assumed to be the identity matrix.
static bool matrixEquals(Matrix4 a, Matrix4 b) {
if (identical(a, b))
return true;
assert(a != null || b != null);
if (a == null)
return isIdentity(b);
if (b == null)
return isIdentity(a);
assert(a != null && b != null);
return a.storage[0] == b.storage[0]
&& a.storage[1] == b.storage[1]
&& a.storage[2] == b.storage[2]
&& a.storage[3] == b.storage[3]
&& a.storage[4] == b.storage[4]
&& a.storage[5] == b.storage[5]
&& a.storage[6] == b.storage[6]
&& a.storage[7] == b.storage[7]
&& a.storage[8] == b.storage[8]
&& a.storage[9] == b.storage[9]
&& a.storage[10] == b.storage[10]
&& a.storage[11] == b.storage[11]
&& a.storage[12] == b.storage[12]
&& a.storage[13] == b.storage[13]
&& a.storage[14] == b.storage[14]
&& a.storage[15] == b.storage[15];
}
static bool isIdentity(Matrix4 a) {
assert(a != null);
return a.storage[0] == 1.0 // col 1
&& a.storage[1] == 0.0
&& a.storage[2] == 0.0
&& a.storage[3] == 0.0
&& a.storage[4] == 0.0 // col 2
&& a.storage[5] == 1.0
&& a.storage[6] == 0.0
&& a.storage[7] == 0.0
&& a.storage[8] == 0.0 // col 3
&& a.storage[9] == 0.0
&& a.storage[10] == 1.0
&& a.storage[11] == 0.0
&& a.storage[12] == 0.0 // col 4
&& a.storage[13] == 0.0
&& a.storage[14] == 0.0
&& a.storage[15] == 1.0;
}
static Point transformPoint(Matrix4 transform, Point point) {
Vector3 position3 = new Vector3(point.x, point.y, 0.0);
Vector3 transformed3 = transform.transform3(position3);
return new Point(transformed3.x, transformed3.y);
}
static double _min4(double a, double b, double c, double d) {
return math.min(a, math.min(b, math.min(c, d)));
}
static double _max4(double a, double b, double c, double d) {
return math.max(a, math.max(b, math.max(c, d)));
}
static Rect transformRect(Rect rect, Matrix4 transform) {
assert(rect != null);
assert(transform.determinant != 0.0);
if (isIdentity(transform))
return rect;
transform = new Matrix4.copy(transform)..invert();
Point point1 = transformPoint(transform, rect.topLeft);
Point point2 = transformPoint(transform, rect.topRight);
Point point3 = transformPoint(transform, rect.bottomLeft);
Point point4 = transformPoint(transform, rect.bottomRight);
return new Rect.fromLTRB(
_min4(point1.x, point2.x, point3.x, point4.x),
_min4(point1.y, point2.y, point3.y, point4.y),
_max4(point1.x, point2.x, point3.x, point4.x),
_max4(point1.y, point2.y, point3.y, point4.y)
);
}
}

View file

@ -0,0 +1,112 @@
Flutter Rendering Layer
=======================
This document is intended to describe some of the core designs of the
Flutter rendering layer.
Layout
------
Paint
-----
Compositing
-----------
Semantics
---------
The last phase of a frame is the Semantics phase. This only occurs if
a semantics server has been installed, for example if the user is
using an accessibility tool.
Each frame, the semantics phase starts with a call to the static
`RenderObject.flushSemantics()` method from the `Renderer` binding's
`beginFrame()` method.
Each node marked as needing semantics (which initially is just the
root node, as scheduled by `scheduleInitialSemantics()`), in depth
order, has its semantics updated by calling `_updateSemantics()`.
The `_updateSemantics()` method calls `_getSemantics()` to obtain an
`_InterestingSemanticsFragment`, and then calls `compile()` on that
fragment to obtain a `SemanticsNode` which becomes the value of the
`RenderObject`'s `_semantics` field. **This is essentially a two-pass
walk of the render tree. The first pass determines the shape of the
output tree, and the second creates the nodes of this tree and hooks
them together.** The second walk is a sparse walk; it only walks the
nodes that are interesting for the purpose of semantics.
`_getSemantics()` is the core function that walks the render tree to
obtain the semantics. It collects semantic annotators for this
`RenderObject`, then walks its children collecting
`_SemanticsFragment`s for them, and then returns an appropriate
`_SemanticsFragment` object that describes the `RenderObject`'s
semantics.
Semantic annotators are functions that, given a `SemanticsNode`, set
some flags or strings on the object. They are obtained from
`getSemanticAnnotators()`. For example, here is how `RenderParagraph`
annotates the `SemanticsNode` with its text:
```dart
Iterable<SemanticAnnotator> getSemanticAnnotators() sync* {
yield (SemanticsNode node) {
node.label = text.toPlainText();
};
}
```
A `_SemanticsFragment` object is a node in a short-lived tree which is
used to create the final `SemanticsNode` tree that is sent to the
semantics server. These objects have a list of semantic annotators,
and a list of `_SemanticsFragment` children.
There are several `_SemanticsFragment` classes. The `_getSemantics()`
method picks its return value as follows:
* `_CleanSemanticsFragment` is used to represent a `RenderObject` that
has a `SemanticsNode` and which is in no way dirty. This class has
no children and no annotators, and when compiled, it returns the
`SemanticsNode` that the `RenderObject` already has.
* `_RootSemanticsFragment`* is used to represent the `RenderObject`
found at the top of the render tree. This class always compiles to a
`SemanticsNode` with ID 0.
* `_ConcreteSemanticsFragment`* is used to represent a `RenderObject`
that has `hasSemantics` set to true. It returns the `SemanticsNode`
for that `RenderObject`.
* `_ImplicitSemanticsFragment`* is used to represent a `RenderObject`
that does not have `hasSemantics` set to true, but which does have
some semantic annotators. When it is compiled, if the nearest
ancestor `_SemanticsFragment` that isn't also an
`_ImplicitSemanticsFragment` is a `_RootSemanticsFragment` or a
`_ConcreteSemanticsFragment`, then the `SemanticsNode` from that
object is reused. Otherwise, a new one is created.
* `_ForkingSemanticsFragment` is used to represent a `RenderObject`
that introduces no semantics of its own, but which has two or more
descendants that do introduce semantics (and which are not ancestors
or descendants of each other).
* For `RenderObject` nodes that introduce no semantics but which have
a (single) child that does, the `_SemanticsFragment` of the child is
returned.
* For `RenderObject` nodes that introduce no semantics and have no
descendants that introduce semantics, `null` is returned.
The classes marked with an asterisk * above are the
`_InterestingSemanticsFragment` classes.
When the `_SemanticsFragment` tree is then compiled, the
`SemanticsNode` objects are created (if necessary), the semantic
annotators are run on each `SemanticsNode`, the geometry (matrix,
size, and clip) is applied, and the children are updated.
As part of this, the code clears out the `_semantics` field of any
`RenderObject` that previously had a `SemanticsNode` but no longer
does. This is done as part of the first walk where possible, and as
part of the second otherwise.

View file

@ -16,3 +16,33 @@ export 'dart:ui' show
VoidCallback;
typedef void ValueChanged<T>(T value);
/// A BitField over an enum (or other class whose values implement "index").
/// Only the first 63 values of the enum can be used as indices.
class BitField<T extends dynamic> {
static const _kSMIBits = 63; // see https://www.dartlang.org/articles/numeric-computation/#smis-and-mints
static const _kAllZeros = 0;
static const _kAllOnes = 0x7FFFFFFFFFFFFFFF; // 2^(_kSMIBits+1)-1
BitField(this._length) : _bits = _kAllZeros {
assert(_length <= _kSMIBits);
}
BitField.filled(this._length, bool value) : _bits = value ? _kAllOnes : _kAllZeros {
assert(_length <= _kSMIBits);
}
final int _length;
int _bits;
bool operator [](T index) {
assert(index.index < _length);
return (_bits & 1 << index.index) > 0;
}
void operator []=(T index, bool value) {
assert(index.index < _length);
if (value)
_bits = _bits | (1 << index.index);
else
_bits = _bits & ~(1 << index.index);
}
void reset([ bool value = false ]) {
_bits = value ? _kAllOnes : _kAllZeros;
}
}

View file

@ -7,11 +7,13 @@ import 'dart:ui' as ui;
import 'package:flutter/gestures.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:sky_services/semantics/semantics.mojom.dart' as mojom;
import 'box.dart';
import 'debug.dart';
import 'object.dart';
import 'view.dart';
import 'semantics.dart';
export 'package:flutter/gestures.dart' show HitTestResult;
@ -39,6 +41,8 @@ abstract class Renderer extends Scheduler
if (renderView == null) {
renderView = new RenderView();
renderView.scheduleInitialFrame();
if (_semanticsClient != null)
renderView.scheduleInitialSemantics();
}
handleMetricsChanged(); // configures renderView's metrics
}
@ -61,6 +65,14 @@ abstract class Renderer extends Scheduler
renderView.configuration = new ViewConfiguration(size: ui.window.size);
}
mojom.SemanticsClient _semanticsClient;
void setSemanticsClient(mojom.SemanticsClient client) {
assert(_semanticsClient == null);
_semanticsClient = client;
if (renderView != null)
renderView.scheduleInitialSemantics();
}
void _handlePersistentFrameCallback(Duration timeStamp) {
beginFrame();
}
@ -71,7 +83,11 @@ abstract class Renderer extends Scheduler
RenderObject.flushLayout();
RenderObject.flushCompositingBits();
RenderObject.flushPaint();
renderView.compositeFrame();
renderView.compositeFrame(); // this sends the bits to the GPU
if (_semanticsClient != null) {
RenderObject.flushSemantics();
SemanticsNode.sendSemanticsTreeTo(_semanticsClient);
}
}
void hitTest(HitTestResult result, Point position) {
@ -91,6 +107,13 @@ void debugDumpLayerTree() {
debugPrint(Renderer.instance?.renderView?.layer?.toStringDeep());
}
/// Prints a textual representation of the entire semantics tree.
/// This will only work if there is a semantics client attached.
/// Otherwise, the tree is empty and this will print "null".
void debugDumpSemanticsTree() {
debugPrint(Renderer.instance?.renderView?.debugSemantics?.toStringDeep());
}
/// A concrete binding for applications that use the Rendering framework
/// directly. This is the glue that binds the framework to the Flutter engine.
class RenderingFlutterBinding extends BindingBase with Scheduler, Gesturer, Renderer {

View file

@ -341,6 +341,7 @@ class RenderBlockViewport extends RenderBlockBase {
if (value != _startOffset) {
_startOffset = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
}
@ -430,6 +431,8 @@ class RenderBlockViewport extends RenderBlockBase {
super.applyPaintTransform(child, transform);
}
Rect describeApproximatePaintClip(RenderObject child) => Point.origin & size;
bool hitTestChildren(HitTestResult result, { Point position }) {
if (isVertical)
return defaultHitTestChildren(result, position: position + new Offset(0.0, -startOffset));

View file

@ -450,6 +450,8 @@ abstract class RenderBox extends RenderObject {
assert(debugDoesMeetConstraints());
}
Rect get semanticBounds => Point.origin & size;
void debugResetSize() {
// updates the value of size._canBeUsedByParent if necessary
size = size;
@ -627,12 +629,6 @@ abstract class RenderBox extends RenderObject {
/// visually "on top" (i.e., paints later).
bool hitTestChildren(HitTestResult result, { Point position }) => false;
static Point _transformPoint(Matrix4 transform, Point point) {
Vector3 position3 = new Vector3(point.x, point.y, 0.0);
Vector3 transformed3 = transform.transform3(position3);
return new Point(transformed3.x, transformed3.y);
}
/// Multiply the transform from the parent's coordinate system to this box's
/// coordinate system into the given transform.
///
@ -666,7 +662,7 @@ abstract class RenderBox extends RenderObject {
double det = transform.invert();
if (det == 0.0)
return Point.origin;
return _transformPoint(transform, point);
return MatrixUtils.transformPoint(transform, point);
}
/// Convert the given point from the local coordinate system for this box to
@ -678,7 +674,7 @@ abstract class RenderBox extends RenderObject {
Matrix4 transform = new Matrix4.identity();
for (int index = renderers.length - 1; index > 0; index -= 1)
renderers[index].applyPaintTransform(renderers[index - 1], transform);
return _transformPoint(transform, point);
return MatrixUtils.transformPoint(transform, point);
}
/// Returns a rectangle that contains all the pixels painted by this box.

View file

@ -48,7 +48,7 @@ bool debugPaintLayerBordersEnabled = false;
/// The color to use when painting Layer borders.
ui.Color debugPaintLayerBordersColor = const ui.Color(0xFFFF9800);
/// Causes RenderBox objects to flash while they are being tapped
/// Causes RenderBox objects to flash while they are being tapped.
bool debugPaintPointersEnabled = false;
/// The color to use when reporting pointers.

View file

@ -169,11 +169,14 @@ class RenderEditableLine extends RenderBox {
}
}
bool get _hasVisualOverflow => _contentSize.width > size.width;
void paint(PaintingContext context, Offset offset) {
final bool hasVisualOverflow = (_contentSize.width > size.width);
if (hasVisualOverflow)
if (_hasVisualOverflow)
context.pushClipRect(needsCompositing, offset, Point.origin & size, _paintContents);
else
_paintContents(context, offset);
}
Rect describeApproximatePaintClip(RenderObject child) => _hasVisualOverflow ? Point.origin & size : null;
}

View file

@ -587,6 +587,8 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, Fl
});
}
Rect describeApproximatePaintClip(RenderObject child) => _overflow > 0.0 ? Point.origin & size : null;
String toString() {
String header = super.toString();
if (_overflow is double && _overflow > 0.0)

View file

@ -6,6 +6,7 @@ import 'dart:developer';
import 'dart:ui' as ui;
import 'package:flutter/gestures.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:vector_math/vector_math_64.dart';
@ -13,6 +14,8 @@ import 'package:vector_math/vector_math_64.dart';
import 'debug.dart';
import 'layer.dart';
import 'node.dart';
import 'semantics.dart';
import 'binding.dart';
export 'layer.dart';
export 'package:flutter/gestures.dart' show HitTestEntry, HitTestResult;
@ -361,6 +364,281 @@ typedef void RenderingExceptionHandler(RenderObject source, String method, dynam
/// information will be printed to the console instead.
RenderingExceptionHandler debugRenderingExceptionHandler;
class _SemanticsGeometry {
_SemanticsGeometry() : transform = new Matrix4.identity();
_SemanticsGeometry.withClipFrom(_SemanticsGeometry other) {
clipRect = other?.clipRect;
transform = new Matrix4.identity();
}
_SemanticsGeometry.copy(_SemanticsGeometry other) {
if (other != null) {
clipRect = other.clipRect;
transform = new Matrix4.copy(other.transform);
} else {
transform = new Matrix4.identity();
}
}
Rect clipRect;
Rect _intersectClipRect(Rect other) {
if (clipRect == null)
return other;
if (other == null)
return clipRect;
return clipRect.intersect(other);
}
Matrix4 transform;
void applyAncestorChain(List<RenderObject> ancestorChain) {
for (int index = ancestorChain.length-1; index > 0; index -= 1) {
RenderObject parent = ancestorChain[index];
RenderObject child = ancestorChain[index-1];
clipRect = _intersectClipRect(parent.describeApproximatePaintClip(child));
if (clipRect != null) {
if (clipRect.isEmpty) {
clipRect = Rect.zero;
} else {
Matrix4 clipTransform = new Matrix4.identity();
parent.applyPaintTransform(child, clipTransform);
clipRect = MatrixUtils.transformRect(clipRect, clipTransform);
}
}
parent.applyPaintTransform(child, transform);
}
}
void updateSemanticsNode({ RenderObject rendering, SemanticsNode semantics, SemanticsNode parentSemantics }) {
assert(rendering != null);
assert(semantics != null);
assert(parentSemantics.wasAffectedByClip != null);
semantics.transform = transform;
if (clipRect != null) {
semantics.rect = clipRect.intersect(rendering.semanticBounds);
semantics.wasAffectedByClip = true;
} else {
semantics.rect = rendering.semanticBounds;
semantics.wasAffectedByClip = parentSemantics?.wasAffectedByClip ?? false;
}
}
}
abstract class _SemanticsFragment {
_SemanticsFragment({
RenderObject owner,
Iterable<SemanticAnnotator> annotators,
List<_SemanticsFragment> children
}) {
assert(owner != null);
_ancestorChain = <RenderObject>[owner];
if (annotators != null)
addAnnotators(annotators);
assert(() {
if (children == null)
return true;
Set<_SemanticsFragment> seenChildren = new Set<_SemanticsFragment>();
for (_SemanticsFragment child in children)
assert(seenChildren.add(child)); // check for duplicate adds
return true;
});
_children = children ?? const <_SemanticsFragment>[];
}
List<RenderObject> _ancestorChain;
void addAncestor(RenderObject ancestor) {
_ancestorChain.add(ancestor);
}
RenderObject get owner => _ancestorChain.first;
List<SemanticAnnotator> _annotators;
void addAnnotators(Iterable<SemanticAnnotator> moreAnnotators) {
if (_annotators == null)
_annotators = moreAnnotators is List<SemanticAnnotator> ? moreAnnotators : moreAnnotators.toList();
else
_annotators.addAll(moreAnnotators);
}
List<_SemanticsFragment> _children;
bool _debugCompiled = false;
Iterable<SemanticsNode> compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics });
String toString() => '$runtimeType($hashCode)';
}
/// Represents a subtree that doesn't need updating, it already has a
/// SemanticsNode and isn't dirty. (We still update the matrix, since
/// that comes from the (dirty) ancestors.)
class _CleanSemanticsFragment extends _SemanticsFragment {
_CleanSemanticsFragment({
RenderObject owner
}) : super(owner: owner) {
assert(owner._semantics != null);
}
Iterable<SemanticsNode> compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics }) sync* {
assert(!_debugCompiled);
assert(() { _debugCompiled = true; return true; });
SemanticsNode node = owner._semantics;
assert(node != null);
if (geometry != null) {
geometry.applyAncestorChain(_ancestorChain);
geometry.updateSemanticsNode(rendering: owner, semantics: node, parentSemantics: parentSemantics);
} else {
assert(_ancestorChain.length == 1);
}
yield node;
}
}
abstract class _InterestingSemanticsFragment extends _SemanticsFragment {
_InterestingSemanticsFragment({
RenderObject owner,
Iterable<SemanticAnnotator> annotators,
Iterable<_SemanticsFragment> children
}) : super(owner: owner, annotators: annotators, children: children);
bool get haveConcreteNode => true;
Iterable<SemanticsNode> compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics }) sync* {
assert(!_debugCompiled);
assert(() { _debugCompiled = true; return true; });
SemanticsNode node = establishSemanticsNode(geometry, currentSemantics, parentSemantics);
for (SemanticAnnotator annotator in _annotators)
annotator(node);
for (_SemanticsFragment child in _children) {
assert(child._ancestorChain.last == owner);
node.addChildren(child.compile(
geometry: createSemanticsGeometryForChild(geometry),
currentSemantics: _children.length > 1 ? null : node,
parentSemantics: node
));
}
if (haveConcreteNode) {
node.finalizeChildren();
yield node;
}
}
SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics);
_SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry);
}
class _RootSemanticsFragment extends _InterestingSemanticsFragment {
_RootSemanticsFragment({
RenderObject owner,
Iterable<SemanticAnnotator> annotators,
Iterable<_SemanticsFragment> children
}) : super(owner: owner, annotators: annotators, children: children);
SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics) {
assert(_ancestorChain.length == 1);
assert(geometry == null);
assert(currentSemantics == null);
assert(parentSemantics == null);
owner._semantics ??= new SemanticsNode.root(
handler: owner is SemanticActionHandler ? owner as dynamic : null
);
SemanticsNode node = owner._semantics;
assert(MatrixUtils.matrixEquals(node.transform, new Matrix4.identity()));
assert(!node.wasAffectedByClip);
node.rect = owner.semanticBounds;
return node;
}
_SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry) {
return new _SemanticsGeometry();
}
}
class _ConcreteSemanticsFragment extends _InterestingSemanticsFragment {
_ConcreteSemanticsFragment({
RenderObject owner,
Iterable<SemanticAnnotator> annotators,
Iterable<_SemanticsFragment> children
}) : super(owner: owner, annotators: annotators, children: children);
SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics) {
owner._semantics ??= new SemanticsNode(
handler: owner is SemanticActionHandler ? owner as dynamic : null
);
SemanticsNode node = owner._semantics;
if (geometry != null) {
geometry.applyAncestorChain(_ancestorChain);
geometry.updateSemanticsNode(rendering: owner, semantics: node, parentSemantics: parentSemantics);
} else {
assert(_ancestorChain.length == 1);
}
return node;
}
_SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry) {
return new _SemanticsGeometry.withClipFrom(geometry);
}
}
class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment {
_ImplicitSemanticsFragment({
RenderObject owner,
Iterable<SemanticAnnotator> annotators,
Iterable<_SemanticsFragment> children
}) : super(owner: owner, annotators: annotators, children: children);
bool get haveConcreteNode => _haveConcreteNode;
bool _haveConcreteNode;
SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics) {
SemanticsNode node;
assert(_haveConcreteNode == null);
_haveConcreteNode = currentSemantics == null && _annotators.isNotEmpty;
if (haveConcreteNode) {
owner._semantics ??= new SemanticsNode(
handler: owner is SemanticActionHandler ? owner as dynamic : null
);
node = owner._semantics;
} else {
owner._semantics = null;
node = currentSemantics;
}
if (geometry != null) {
geometry.applyAncestorChain(_ancestorChain);
if (haveConcreteNode)
geometry.updateSemanticsNode(rendering: owner, semantics: node, parentSemantics: parentSemantics);
} else {
assert(_ancestorChain.length == 1);
}
return node;
}
_SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry) {
if (haveConcreteNode)
return new _SemanticsGeometry.withClipFrom(geometry);
return new _SemanticsGeometry.copy(geometry);
}
}
class _ForkingSemanticsFragment extends _SemanticsFragment {
_ForkingSemanticsFragment({
RenderObject owner,
Iterable<_SemanticsFragment> children
}) : super(owner: owner, children: children) {
assert(children != null);
assert(children.length > 1);
}
Iterable<SemanticsNode> compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics }) sync* {
assert(!_debugCompiled);
assert(() { _debugCompiled = true; return true; });
assert(geometry != null);
geometry.applyAncestorChain(_ancestorChain);
for (_SemanticsFragment child in _children) {
assert(child._ancestorChain.last == owner);
yield* child.compile(
geometry: new _SemanticsGeometry.copy(geometry),
currentSemantics: null,
parentSemantics: parentSemantics
);
}
}
}
/// An object in the render tree.
///
/// Render objects have a reference to their parent but do not commit to a model
@ -448,16 +726,17 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
debugPrint('This RenderObject had the following owner:\n$debugOwner');
int depth = 0;
List<String> descendants = <String>[];
const int maxDepth = 5;
void visitor(RenderObject child) {
descendants.add('${" " * depth}$child');
depth += 1;
if (depth < 5)
if (depth < maxDepth)
child.visitChildren(visitor);
depth -= 1;
}
visitChildren(visitor);
if (descendants.length > 1) {
debugPrint('This RenderObject had the following descendants:');
debugPrint('This RenderObject had the following descendants (showing up to depth $maxDepth):');
} else if (descendants.length == 1) {
debugPrint('This RenderObject had the following child:');
} else {
@ -651,6 +930,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
});
try {
performLayout();
markNeedsSemanticsUpdate();
} catch (e, stack) {
_debugReportException('performLayout', e, stack);
}
@ -744,6 +1024,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
});
try {
performLayout();
markNeedsSemanticsUpdate();
assert(debugDoesMeetConstraints());
} catch (e, stack) {
_debugReportException('performLayout', e, stack);
@ -1120,6 +1401,240 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
assert(child.parent == this);
}
/// Returns a rect in this object's coordinate system that describes
/// the approximate bounding box of the clip rect that would be
/// applied to the given child during the paint phase, if any.
///
/// Returns null if the child would not be clipped.
///
/// This is used in the semantics phase to avoid including children
/// that are not physically visible.
Rect describeApproximatePaintClip(RenderObject child) => null;
// SEMANTICS
static bool _semanticsEnabled = false;
static bool _debugDoingSemantics = false;
static List<RenderObject> _nodesNeedingSemantics = <RenderObject>[];
/// Bootstrap the semantics reporting mechanism by marking this node
/// as needing a semantics update.
///
/// Requires that this render object is attached, and is the root of
/// the render tree.
///
/// See [Renderer] for an example of how this function is used.
void scheduleInitialSemantics() {
assert(attached);
assert(parent is! RenderObject);
assert(!_debugDoingSemantics);
assert(_semantics == null);
assert(_needsSemanticsUpdate);
assert(_semanticsEnabled == false);
_semanticsEnabled = true;
_nodesNeedingSemantics.add(this);
Scheduler.instance.ensureVisualUpdate();
}
static void flushSemantics() {
Timeline.startSync('Semantics');
assert(_semanticsEnabled);
assert(() { _debugDoingSemantics = true; return true; });
try {
_nodesNeedingSemantics.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
for (RenderObject node in _nodesNeedingSemantics) {
if (node._needsSemanticsUpdate)
node._updateSemantics();
}
} finally {
_nodesNeedingSemantics.clear();
assert(() { _debugDoingSemantics = false; return true; });
Timeline.finishSync();
}
}
/// Whether this RenderObject introduces a new box for accessibility purposes.
bool get hasSemantics => false;
/// The bounding box, in the local coordinate system, of this
/// object, for accessibility purposes.
Rect get semanticBounds;
bool _needsSemanticsUpdate = true;
bool _needsSemanticsGeometryUpdate = true;
SemanticsNode _semantics;
SemanticsNode get debugSemantics { // only exposed for testing and debugging
SemanticsNode result;
assert(() {
result = _semantics;
return true;
});
return result;
}
/// Mark this node as needing an update to its semantics
/// description.
///
/// If the change did not involve a removal or addition of
/// semantics, only the change of semantics (e.g. isChecked changing
/// from true to false, as opposed to isChecked changing from being
/// true to not being changed at all), then you can pass the
/// onlyChanges argument with the value true to reduce the cost. If
/// semantics are being added or removed, more work needs to be done
/// to update the semantics tree. If you pass 'onlyChanges: true'
/// but this node, which previously had a SemanticsNode, no longer
/// has one, or previously did not set any semantics, but now does,
/// or previously had a child that returned annotators, but no
/// longer does, or other such combinations, then you will either
/// assert during the subsequent call to [flushSemantics()] or you
/// will have out-of-date information in the semantics tree.
///
/// If the geometry might have changed in any way, then again, more
/// work needs to be done to update the semantics tree (to deal with
/// clips). You can pass the noGeometry argument to avoid this work
/// in the case where only the labels or flags changed. If you pass
/// 'noGeometry: true' when the geometry did change, the semantic
/// tree will be out of date.
void markNeedsSemanticsUpdate({ bool onlyChanges: false, bool noGeometry: false }) {
assert(!_debugDoingSemantics);
if (!_semanticsEnabled || !attached || (_needsSemanticsUpdate && onlyChanges && (_needsSemanticsGeometryUpdate || noGeometry)))
return;
if (!noGeometry && (_semantics == null || (_semantics.hasChildren && _semantics.wasAffectedByClip))) {
// Since the geometry might have changed, we need to make sure to reapply any clips.
_needsSemanticsGeometryUpdate = true;
}
if (onlyChanges) {
// The shape of the tree didn't change, but the details did.
// If we have our own SemanticsNode (our _semantics isn't null)
// then mark ourselves dirty. If we don't then we are using an
// ancestor's; mark all the nodes up to that one dirty.
RenderObject node = this;
while (node._semantics == null && node.parent is RenderObject) {
if (node._needsSemanticsUpdate)
return;
node._needsSemanticsUpdate = true;
node = node.parent;
}
if (!node._needsSemanticsUpdate) {
node._needsSemanticsUpdate = true;
_nodesNeedingSemantics.add(node);
}
} else {
// The shape of the semantics tree around us may have changed.
// The worst case is that we may have removed a branch of the
// semantics tree, because when that happens we have to go up
// and dirty the nearest _semantics-laden ancestor of the
// affected node to rebuild the tree.
RenderObject node = this;
do {
if (node.parent is! RenderObject)
break;
node._needsSemanticsUpdate = true;
node._semantics?.reset();
node = node.parent;
} while (node._semantics == null);
node._semantics?.reset();
if (!node._needsSemanticsUpdate) {
node._needsSemanticsUpdate = true;
_nodesNeedingSemantics.add(node);
}
}
}
void _updateSemantics() {
try {
assert(_needsSemanticsUpdate);
assert(_semantics != null || parent is! RenderObject);
_SemanticsFragment fragment = _getSemanticsFragment();
assert(fragment is _InterestingSemanticsFragment);
SemanticsNode node = fragment.compile(parentSemantics: _semantics?.parent).single;
assert(node != null);
assert(node == _semantics);
} catch (e, stack) {
_debugReportException('_updateSemantics', e, stack);
}
}
_SemanticsFragment _getSemanticsFragment() {
// early-exit if we're not dirty and have our own semantics
if (!_needsSemanticsUpdate && hasSemantics) {
assert(_semantics != null);
return new _CleanSemanticsFragment(owner: this);
}
List<_SemanticsFragment> children;
visitChildrenForSemantics((RenderObject child) {
if (_needsSemanticsGeometryUpdate) {
// If our geometry changed, make sure the child also does a
// full update so that any changes to the clip are fully
// applied.
child._needsSemanticsUpdate = true;
child._needsSemanticsGeometryUpdate = true;
}
_SemanticsFragment fragment = child._getSemanticsFragment();
if (fragment != null) {
fragment.addAncestor(this);
children ??= <_SemanticsFragment>[];
assert(!children.contains(fragment));
children.add(fragment);
}
});
_needsSemanticsUpdate = false;
_needsSemanticsGeometryUpdate = false;
Iterable<SemanticAnnotator> annotators = getSemanticAnnotators();
if (parent is! RenderObject)
return new _RootSemanticsFragment(owner: this, annotators: annotators, children: children);
if (hasSemantics)
return new _ConcreteSemanticsFragment(owner: this, annotators: annotators, children: children);
if (annotators.isNotEmpty)
return new _ImplicitSemanticsFragment(owner: this, annotators: annotators, children: children);
_semantics = null;
if (children == null)
return null;
if (children.length > 1)
return new _ForkingSemanticsFragment(owner: this, children: children);
assert(children.length == 1);
return children.single;
}
/// Called when collecting the semantics of this node. Subclasses
/// that have children that are not semantically relevant (e.g.
/// because they are invisible) should skip those children here.
///
/// The default implementation mirrors the behavior of
/// [visitChildren()] (which is supposed to walk all the children).
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
visitChildren(visitor);
}
/// Returns functions that will annotate a SemanticsNode with the
/// semantics of this RenderObject.
///
/// To annotate a SemanticsNode for this node, return all the
/// annotators provided by the superclass, plus an annotator that
/// adds the annotations. When the behavior of the annotators would
/// change (e.g. the box is now checked rather than unchecked), call
/// [markNeedsSemanticsUpdate()] to indicate to the rendering system
/// that the semantics tree needs to be rebuilt.
///
/// To introduce a new SemanticsNode, set hasSemantics to true for
/// this object. The functions returned by this function will be used
/// to annotate the SemanticsNode for this object.
///
/// Semantic annotations are persistent. Values set in one pass will
/// still be set in the next pass. Therefore it is important to
/// explicitly set fields to false once they are no longer true --
/// setting them to true when they are to be enabled, and not
/// setting them at all when they are not, will mean they remain set
/// once enabled once and will never get unset.
///
/// If the number of annotators you return will change from zero to
/// non-zero, and hasSemantics isn't true, then the associated call
/// to markNeedsSemanticsUpdate() must not have 'onlyChanges' set, as
/// it is possible that the node should be entirely removed.
Iterable<SemanticAnnotator> getSemanticAnnotators() sync* { }
// EVENTS

View file

@ -236,4 +236,5 @@ class RenderOffStage extends RenderBox with RenderObjectWithChildMixin<RenderBox
bool hitTest(HitTestResult result, { Point position }) => false;
void paint(PaintingContext context, Offset offset) { }
void visitChildrenForSemantics(RenderObjectVisitor visitor) { }
}

View file

@ -6,6 +6,7 @@ import 'package:flutter/painting.dart';
import 'box.dart';
import 'object.dart';
import 'semantics.dart';
export 'package:flutter/painting.dart' show
FontStyle,
@ -111,7 +112,11 @@ class RenderParagraph extends RenderBox {
_textPainter.paint(context.canvas, offset);
}
// we should probably expose a way to do precise (inter-glpyh) hit testing
Iterable<SemanticAnnotator> getSemanticAnnotators() sync* {
yield (SemanticsNode node) {
node.label = text.toPlainText();
};
}
String debugDescribeChildren(String prefix) {
return '$prefix \u2558\u2550\u2566\u2550\u2550 text \u2550\u2550\u2550\n'

View file

@ -11,6 +11,7 @@ import 'package:vector_math/vector_math_64.dart';
import 'box.dart';
import 'debug.dart';
import 'object.dart';
import 'semantics.dart';
export 'package:flutter/gestures.dart' show
PointerEvent,
@ -557,6 +558,7 @@ class RenderOpacity extends RenderProxyBox {
_alpha = _getAlphaFromOpacity(_opacity);
markNeedsCompositingBitsUpdate();
markNeedsPaint();
markNeedsSemanticsUpdate();
}
int _alpha;
@ -574,6 +576,11 @@ class RenderOpacity extends RenderProxyBox {
}
}
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (child != null && _alpha != 0)
visitor(child);
}
void debugDescribeSettings(List<String> settings) {
super.debugDescribeSettings(settings);
settings.add('opacity: ${opacity.toStringAsFixed(1)}');
@ -622,6 +629,17 @@ abstract class CustomClipper<T> {
/// Returns a description of the clip given that the render object being
/// clipped is of the given size.
T getClip(Size size);
/// Returns an approximation of the clip returned by [getClip], as
/// an axis-aligned Rect. This is used by the semantics layer to
/// determine whether widgets should be excluded.
///
/// By default, this returns a rectangle that is the same size as
/// the RenderObject. If getClip returns a shape that is roughly the
/// same size as the RenderObject (e.g. it's a rounded rectangle
/// with very small arcs in the corners), then this may be adequate.
Rect getApproximateClipRect(Size size) => Point.origin & size;
/// Returns true if the new instance will result in a different clip
/// than the oldClipper instance.
bool shouldRepaint(CustomClipper oldClipper);
}
@ -642,15 +660,19 @@ abstract class _RenderCustomClip<T> extends RenderProxyBox {
if (newClipper == null) {
assert(oldClipper != null);
markNeedsPaint();
markNeedsSemanticsUpdate(onlyChanges: true);
} else if (oldClipper == null ||
oldClipper.runtimeType != oldClipper.runtimeType ||
newClipper.shouldRepaint(oldClipper)) {
markNeedsPaint();
markNeedsSemanticsUpdate(onlyChanges: true);
}
}
T get _defaultClip;
T get _clip => _clipper?.getClip(size) ?? _defaultClip;
Rect describeApproximatePaintClip(RenderObject child) => _clipper?.getApproximateClipRect(size) ?? Point.origin & size;
}
/// Clips its child using a rectangle.
@ -722,6 +744,9 @@ class RenderClipRRect extends RenderProxyBox {
markNeedsPaint();
}
// TODO(ianh): either convert this to the CustomClipper world, or
// TODO(ianh): implement describeApproximatePaintClip for this class
void paint(PaintingContext context, Offset offset) {
if (child != null) {
Rect rect = Point.origin & size;
@ -1314,18 +1339,59 @@ class RenderRepaintBoundary extends RenderProxyBox {
/// to hit testing. It still consumes space during layout and paints its child
/// as usual. It just cannot be the target of located events because it returns
/// false from [hitTest].
///
/// When [ignoringSemantics] is true, the subtree will be invisible to
/// the semantics layer (and thus e.g. accessibility tools). If
/// [ignoringSemantics] is null, it uses the value of [ignoring].
class RenderIgnorePointer extends RenderProxyBox {
RenderIgnorePointer({ RenderBox child, this.ignoring: true }) : super(child);
RenderIgnorePointer({
RenderBox child,
bool ignoring: true,
bool ignoringSemantics
}) : _ignoring = ignoring, _ignoringSemantics = ignoringSemantics, super(child) {
assert(_ignoring != null);
}
bool ignoring;
bool get ignoring => _ignoring;
bool _ignoring;
void set ignoring(bool value) {
assert(value != null);
if (value == _ignoring)
return;
_ignoring = value;
if (ignoringSemantics == null)
markNeedsSemanticsUpdate();
}
bool get ignoringSemantics => _ignoringSemantics;
bool _ignoringSemantics;
void set ignoringSemantics(bool value) {
if (value == _ignoringSemantics)
return;
bool oldEffectiveValue = _effectiveIgnoringSemantics;
_ignoringSemantics = value;
if (oldEffectiveValue != _effectiveIgnoringSemantics)
markNeedsSemanticsUpdate();
}
bool get _effectiveIgnoringSemantics => ignoringSemantics == null ? ignoring : ignoringSemantics;
bool hitTest(HitTestResult result, { Point position }) {
return ignoring ? false : super.hitTest(result, position: position);
}
// TODO(ianh): figure out a way to still include labels and flags in
// descendants, just make them non-interactive, even when
// _effectiveIgnoringSemantics is true
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (child != null && !_effectiveIgnoringSemantics)
visitor(child);
}
void debugDescribeSettings(List<String> settings) {
super.debugDescribeSettings(settings);
settings.add('ignoring: $ignoring');
settings.add('ignoringSemantics: ${ ignoringSemantics == null ? "implicitly " : "" }$_effectiveIgnoringSemantics');
}
}
@ -1336,3 +1402,220 @@ class RenderMetaData extends RenderProxyBox {
/// Opaque meta data ignored by the render tree
dynamic metaData;
}
/// Listens for the specified gestures from the semantics server (e.g.
/// an accessibility tool).
class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticActionHandler {
RenderSemanticsGestureHandler({
RenderBox child,
GestureTapCallback onTap,
GestureLongPressCallback onLongPress,
GestureDragUpdateCallback onHorizontalDragUpdate,
GestureDragUpdateCallback onVerticalDragUpdate,
this.scrollFactor: 0.8
}) : _onTap = onTap,
_onLongPress = onLongPress,
_onHorizontalDragUpdate = onHorizontalDragUpdate,
_onVerticalDragUpdate = onVerticalDragUpdate,
super(child);
GestureTapCallback get onTap => _onTap;
GestureTapCallback _onTap;
void set onTap(GestureTapCallback value) {
if (_onTap == value)
return;
bool didHaveSemantics = hasSemantics;
bool hadHandler = _onTap != null;
_onTap = value;
if ((value != null) != hadHandler)
markNeedsSemanticsUpdate(onlyChanges: hasSemantics == didHaveSemantics);
}
GestureLongPressCallback get onLongPress => _onLongPress;
GestureLongPressCallback _onLongPress;
void set onLongPress(GestureLongPressCallback value) {
if (_onLongPress == value)
return;
bool didHaveSemantics = hasSemantics;
bool hadHandler = _onLongPress != null;
_onLongPress = value;
if ((value != null) != hadHandler)
markNeedsSemanticsUpdate(onlyChanges: hasSemantics == didHaveSemantics);
}
GestureDragUpdateCallback get onHorizontalDragUpdate => _onHorizontalDragUpdate;
GestureDragUpdateCallback _onHorizontalDragUpdate;
void set onHorizontalDragUpdate(GestureDragUpdateCallback value) {
if (_onHorizontalDragUpdate == value)
return;
bool didHaveSemantics = hasSemantics;
bool hadHandler = _onHorizontalDragUpdate != null;
_onHorizontalDragUpdate = value;
if ((value != null) != hadHandler)
markNeedsSemanticsUpdate(onlyChanges: hasSemantics == didHaveSemantics);
}
GestureDragUpdateCallback get onVerticalDragUpdate => _onVerticalDragUpdate;
GestureDragUpdateCallback _onVerticalDragUpdate;
void set onVerticalDragUpdate(GestureDragUpdateCallback value) {
if (_onVerticalDragUpdate == value)
return;
bool didHaveSemantics = hasSemantics;
bool hadHandler = _onVerticalDragUpdate != null;
_onVerticalDragUpdate = value;
if ((value != null) != hadHandler)
markNeedsSemanticsUpdate(onlyChanges: hasSemantics == didHaveSemantics);
}
/// The fraction of the dimension of this render box to use when
/// scrolling. For example, if this is 0.8 and the box is 200 pixels
/// wide, then when a left-scroll action is received from the
/// accessibility system, it will translate into a 160 pixel
/// leftwards drag.
double scrollFactor;
bool get hasSemantics {
return onTap != null
|| onLongPress != null
|| onHorizontalDragUpdate != null
|| onVerticalDragUpdate != null;
}
Iterable<SemanticAnnotator> getSemanticAnnotators() sync* {
if (hasSemantics) {
yield (SemanticsNode semantics) {
semantics.canBeTapped = onTap != null;
semantics.canBeLongPressed = onLongPress != null;
semantics.canBeScrolledHorizontally = onHorizontalDragUpdate != null;
semantics.canBeScrolledVertically = onVerticalDragUpdate != null;
};
}
}
void handleSemanticTap() {
if (onTap != null)
onTap();
}
void handleSemanticLongPress() {
if (onLongPress != null)
onLongPress();
}
void handleSemanticScrollLeft() {
if (onHorizontalDragUpdate != null)
onHorizontalDragUpdate(size.width * -scrollFactor);
}
void handleSemanticScrollRight() {
if (onHorizontalDragUpdate != null)
onHorizontalDragUpdate(size.width * scrollFactor);
}
void handleSemanticScrollUp() {
if (onVerticalDragUpdate != null)
onVerticalDragUpdate(size.height * -scrollFactor);
}
void handleSemanticScrollDown() {
if (onVerticalDragUpdate != null)
onVerticalDragUpdate(size.height * scrollFactor);
}
}
/// Add annotations to the SemanticsNode for this subtree.
class RenderSemanticAnnotations extends RenderProxyBox {
RenderSemanticAnnotations({
RenderBox child,
bool container: false,
bool checked,
String label
}) : _container = container,
_checked = checked,
_label = label,
super(child) {
assert(container != null);
}
/// If 'container' is true, this RenderObject will introduce a new
/// node in the semantics tree. Otherwise, the semantics will be
/// merged with the semantics of any ancestors.
///
/// The 'container' flag is implicitly set to true on the immediate
/// semantics-providing descendants of a node where multiple
/// children have semantics or have descendants providing semantics.
/// In other words, the semantics of siblings are not merged. To
/// merge the semantics of an entire subtree, including siblings,
/// you can use a [RenderMergeSemantics].
bool get container => _container;
bool _container;
void set container(bool value) {
assert(value != null);
if (container == value)
return;
_container = value;
markNeedsSemanticsUpdate();
}
/// If non-null, sets the "hasCheckedState" semantic to true and the
/// "isChecked" semantic to the given value.
bool get checked => _checked;
bool _checked;
void set checked(bool value) {
if (checked == value)
return;
bool hadValue = checked != null;
_checked = value;
markNeedsSemanticsUpdate(onlyChanges: (value != null) == hadValue);
}
/// If non-null, sets the "label" semantic to the given value.
String get label => _label;
String _label;
void set label(String value) {
if (label == value)
return;
bool hadValue = label != null;
_label = value;
markNeedsSemanticsUpdate(onlyChanges: (value != null) == hadValue);
}
bool get hasSemantics => container;
Iterable<SemanticAnnotator> getSemanticAnnotators() sync* {
if (checked != null) {
yield (SemanticsNode semantics) {
semantics.hasCheckedState = true;
semantics.isChecked = checked;
};
}
if (label != null) {
yield (SemanticsNode semantics) {
semantics.label = label;
};
}
}
}
/// Causes the semantics of all descendants to be merged into this
/// node such that the entire subtree becomes a single leaf in the
/// semantics tree.
///
/// Useful for combining the semantics of multiple render objects that
/// form part of a single conceptual widget, e.g. a checkbox, a label,
/// and the gesture detector that goes with them.
class RenderMergeSemantics extends RenderProxyBox {
RenderMergeSemantics({ RenderBox child }) : super(child);
Iterable<SemanticAnnotator> getSemanticAnnotators() sync* {
yield (SemanticsNode node) { node.mergeAllDescendantsIntoThisNode = true; };
}
}
/// Excludes this subtree from the semantic tree.
///
/// Useful e.g. for hiding text that is redundant with other text next
/// to it (e.g. text included only for the visual effect).
class RenderExcludeSemantics extends RenderProxyBox {
RenderExcludeSemantics({ RenderBox child }) : super(child);
void visitChildrenForSemantics(RenderObjectVisitor visitor) { }
}

View file

@ -0,0 +1,448 @@
// Copyright 2015 The Chromium 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:math' as math;
import 'dart:ui' show Rect;
import 'package:flutter/painting.dart';
import 'package:sky_services/semantics/semantics.mojom.dart' as mojom;
import 'package:vector_math/vector_math_64.dart';
import 'basic_types.dart';
import 'node.dart';
/// The type of function returned by [RenderObject.getSemanticAnnotators()].
///
/// These callbacks are invoked with the [SemanticsNode] object that
/// corresponds to the [RenderObject]. (One [SemanticsNode] can
/// correspond to multiple [RenderObject] objects.)
///
/// See [RenderObject.getSemanticAnnotators()] for details on the
/// contract that semantic annotators must follow.
typedef void SemanticAnnotator(SemanticsNode semantics);
/// Interface for RenderObjects to implement when they want to support
/// being tapped, etc.
///
/// These handlers will only be called if the relevant flag is set
/// (e.g. handleSemanticTap() will only be called if canBeTapped is
/// true, handleSemanticScrollDown() will only be called if
/// canBeScrolledVertically is true, etc).
abstract class SemanticActionHandler {
void handleSemanticTap() { }
void handleSemanticLongPress() { }
void handleSemanticScrollLeft() { }
void handleSemanticScrollRight() { }
void handleSemanticScrollUp() { }
void handleSemanticScrollDown() { }
}
enum _SemanticFlags {
mergeAllDescendantsIntoThisNode,
canBeTapped,
canBeLongPressed,
canBeScrolledHorizontally,
canBeScrolledVertically,
hasCheckedState,
isChecked,
}
typedef bool SemanticsNodeVisitor(SemanticsNode node);
class SemanticsNode extends AbstractNode {
SemanticsNode({
SemanticActionHandler handler
}) : _id = _generateNewId(),
_actionHandler = handler;
SemanticsNode.root({
SemanticActionHandler handler
}) : _id = 0,
_actionHandler = handler {
attach();
}
static int _lastIdentifier = 0;
static int _generateNewId() {
_lastIdentifier += 1;
return _lastIdentifier;
}
final int _id;
final SemanticActionHandler _actionHandler;
// GEOMETRY
// These are automatically handled by RenderObject's own logic
Matrix4 get transform => _transform;
Matrix4 _transform; // defaults to null, which we say means the identity matrix
void set transform (Matrix4 value) {
if (!MatrixUtils.matrixEquals(_transform, value)) {
_transform = value;
_markDirty();
}
}
Rect get rect => _rect;
Rect _rect = Rect.zero;
void set rect (Rect value) {
assert(value != null);
if (_rect != value) {
_rect = value;
_markDirty();
}
}
bool wasAffectedByClip = false;
// FLAGS AND LABELS
// These are supposed to be set by SemanticAnnotator obtained from getSemanticAnnotators
BitField<_SemanticFlags> _flags = new BitField<_SemanticFlags>.filled(_SemanticFlags.values.length, false);
void _setFlag(_SemanticFlags flag, bool value, { bool needsHandler: false }) {
assert(value != null);
assert((!needsHandler) || (_actionHandler != null) || (value == false));
if (_flags[flag] != value) {
_flags[flag] = value;
_markDirty();
}
}
bool _canHandle(_SemanticFlags flag) {
return _actionHandler != null && _flags[flag];
}
bool get mergeAllDescendantsIntoThisNode => _flags[_SemanticFlags.mergeAllDescendantsIntoThisNode];
void set mergeAllDescendantsIntoThisNode(bool value) => _setFlag(_SemanticFlags.mergeAllDescendantsIntoThisNode, value);
bool get canBeTapped => _flags[_SemanticFlags.canBeTapped];
void set canBeTapped(bool value) => _setFlag(_SemanticFlags.canBeTapped, value, needsHandler: true);
bool get canBeLongPressed => _flags[_SemanticFlags.canBeLongPressed];
void set canBeLongPressed(bool value) => _setFlag(_SemanticFlags.canBeLongPressed, value, needsHandler: true);
bool get canBeScrolledHorizontally => _flags[_SemanticFlags.canBeScrolledHorizontally];
void set canBeScrolledHorizontally(bool value) => _setFlag(_SemanticFlags.canBeScrolledHorizontally, value, needsHandler: true);
bool get canBeScrolledVertically => _flags[_SemanticFlags.canBeScrolledVertically];
void set canBeScrolledVertically(bool value) => _setFlag(_SemanticFlags.canBeScrolledVertically, value, needsHandler: true);
bool get hasCheckedState => _flags[_SemanticFlags.hasCheckedState];
void set hasCheckedState(bool value) => _setFlag(_SemanticFlags.hasCheckedState, value);
bool get isChecked => _flags[_SemanticFlags.isChecked];
void set isChecked(bool value) => _setFlag(_SemanticFlags.isChecked, value);
String get label => _label;
String _label = '';
void set label(String value) {
assert(value != null);
if (_label != value) {
_label = value;
_markDirty();
}
}
void reset() {
_flags.reset();
_label = '';
_markDirty();
}
List<SemanticsNode> _newChildren;
void addChildren(Iterable<SemanticsNode> children) {
_newChildren ??= <SemanticsNode>[];
_newChildren.addAll(children);
// we do the asserts afterwards because children is an Iterable
// and doing the asserts before would mean the behaviour is
// different in checked mode vs release mode (if you walk an
// iterator after having reached the end, it'll just start over;
// the values are not cached).
assert(!_newChildren.any((SemanticsNode child) => child == this));
assert(() {
SemanticsNode ancestor = this;
while (ancestor.parent is SemanticsNode)
ancestor = ancestor.parent;
assert(!_newChildren.any((SemanticsNode child) => child == ancestor));
return true;
});
assert(() {
Set<SemanticsNode> seenChildren = new Set<SemanticsNode>();
for (SemanticsNode child in _newChildren)
assert(seenChildren.add(child)); // check for duplicate adds
return true;
});
}
List<SemanticsNode> _children;
bool get hasChildren => _children?.isNotEmpty ?? false;
bool _dead = false;
void finalizeChildren() {
if (_children != null) {
for (SemanticsNode child in _children)
child._dead = true;
}
if (_newChildren != null) {
for (SemanticsNode child in _newChildren)
child._dead = false;
}
bool sawChange = false;
if (_children != null) {
for (SemanticsNode child in _children) {
if (child._dead) {
if (child.parent == this) {
// we might have already had our child stolen from us by
// another node that is deeper in the tree.
dropChild(child);
}
sawChange = true;
}
}
}
if (_newChildren != null) {
for (SemanticsNode child in _newChildren) {
if (child.parent != this) {
if (child.parent != null) {
// we're rebuilding the tree from the bottom up, so it's possible
// that our child was, in the last pass, a child of one of our
// ancestors. In that case, we drop the child eagerly here.
// TODO(ianh): Find a way to assert that the same node didn't
// actually appear in the tree in two places.
child.parent?.dropChild(child);
}
assert(!child.attached);
adoptChild(child);
sawChange = true;
}
}
}
List<SemanticsNode> oldChildren = _children;
_children = _newChildren;
oldChildren?.clear();
_newChildren = oldChildren;
if (sawChange)
_markDirty();
}
SemanticsNode get parent => super.parent;
void redepthChildren() {
if (_children != null) {
for (SemanticsNode child in _children)
redepthChild(child);
}
}
// Visits all the descendants of this node, calling visitor for each one, until
// visitor returns false. Returns true if all the visitor calls returned true,
// otherwise returns false.
bool _visitDescendants(SemanticsNodeVisitor visitor) {
if (_children != null) {
for (SemanticsNode child in _children) {
if (!visitor(child) || !child._visitDescendants(visitor))
return false;
}
}
return true;
}
static Map<int, SemanticsNode> _nodes = <int, SemanticsNode>{};
static Set<SemanticsNode> _detachedNodes = new Set<SemanticsNode>();
void attach() {
super.attach();
assert(!_nodes.containsKey(_id));
_nodes[_id] = this;
_detachedNodes.remove(this);
if (_children != null) {
for (SemanticsNode child in _children)
child.attach();
}
}
void detach() {
super.detach();
assert(_nodes.containsKey(_id));
assert(!_detachedNodes.contains(this));
_nodes.remove(_id);
_detachedNodes.add(this);
if (_children != null) {
for (SemanticsNode child in _children)
child.detach();
}
}
static List<SemanticsNode> _dirtyNodes = <SemanticsNode>[];
bool _dirty = false;
void _markDirty() {
if (_dirty)
return;
_dirty = true;
assert(!_dirtyNodes.contains(this));
assert(!_detachedNodes.contains(this));
_dirtyNodes.add(this);
}
mojom.SemanticsNode _serialize() {
mojom.SemanticsNode result = new mojom.SemanticsNode();
result.id = _id;
if (_dirty) {
// We could be even more efficient about not sending data here, by only
// sending the bits that are dirty (tracking the geometry, flags, strings,
// and children separately). For now, we send all or nothing.
result.geometry = new mojom.SemanticGeometry();
result.geometry.transform = transform?.storage;
result.geometry.top = rect.top;
result.geometry.left = rect.left;
result.geometry.width = math.max(rect.width, 0.0);
result.geometry.height = math.max(rect.height, 0.0);
result.flags = new mojom.SemanticFlags();
result.flags.canBeTapped = canBeTapped;
result.flags.canBeLongPressed = canBeLongPressed;
result.flags.canBeScrolledHorizontally = canBeScrolledHorizontally;
result.flags.canBeScrolledVertically = canBeScrolledVertically;
result.flags.hasCheckedState = hasCheckedState;
result.flags.isChecked = isChecked;
result.strings = new mojom.SemanticStrings();
result.strings.label = label;
List<mojom.SemanticsNode> children = <mojom.SemanticsNode>[];
if (mergeAllDescendantsIntoThisNode) {
_visitDescendants((SemanticsNode node) {
result.flags.canBeTapped = result.flags.canBeTapped || node.canBeTapped;
result.flags.canBeLongPressed = result.flags.canBeLongPressed || node.canBeLongPressed;
result.flags.canBeScrolledHorizontally = result.flags.canBeScrolledHorizontally || node.canBeScrolledHorizontally;
result.flags.canBeScrolledVertically = result.flags.canBeScrolledVertically || node.canBeScrolledVertically;
result.flags.hasCheckedState = result.flags.hasCheckedState || node.hasCheckedState;
result.flags.isChecked = result.flags.isChecked || node.isChecked;
if (node.label != '')
result.strings.label = result.strings.label.isNotEmpty ? '${result.strings.label}\n${node.label}' : node.label;
return true; // continue walk
});
// and we pretend to have no children
} else {
if (_children != null) {
for (SemanticsNode child in _children)
children.add(child._serialize());
}
}
result.children = children;
_dirty = false;
}
return result;
}
static void sendSemanticsTreeTo(mojom.SemanticsClient client) {
for (SemanticsNode oldNode in _detachedNodes) {
// The other side will have forgotten this node if we even send
// it again, so make sure to mark it dirty so that it'll get
// sent if it is resurrected.
oldNode._dirty = true;
}
_detachedNodes.clear();
if (_dirtyNodes.isEmpty)
return;
_dirtyNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth);
for (int index = 0; index < _dirtyNodes.length; index += 1) {
// we mutate the list as we walk it here, which is why we use an index instead of an iterator
SemanticsNode node = _dirtyNodes[index];
assert(node._dirty);
assert(node.parent == null || !node.parent.mergeAllDescendantsIntoThisNode || node.mergeAllDescendantsIntoThisNode);
if (node.mergeAllDescendantsIntoThisNode) {
// if we're merged into our parent, make sure our parent is added to the list
if (node.parent != null && node.parent.mergeAllDescendantsIntoThisNode)
node.parent._markDirty(); // this can add the node to the dirty list
// make sure all the descendants are also marked, so that if one gets marked dirty later we know to walk up then too
if (node._children != null)
for (SemanticsNode child in node._children)
child.mergeAllDescendantsIntoThisNode = true; // this can add the node to the dirty list
}
assert(_dirtyNodes[index] == node); // make sure nothing went in front of us in the list
}
_dirtyNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth);
List<mojom.SemanticsNode> updatedNodes = <mojom.SemanticsNode>[];
for (SemanticsNode node in _dirtyNodes) {
assert(node.parent?._dirty != true); // could be null (no parent) or false (not dirty)
// The _serialize() method marks the node as not dirty, and
// recurses through the tree to do a deep serialization of all
// contiguous dirty nodes. This means that when we return here,
// it's quite possible that subsequent nodes are no longer
// dirty. We skip these here.
// We also skip any nodes that were reset and subsequently
// dropped entirely (RenderObject.markNeedsSemanticsUpdate()
// calls reset() on its SemanticsNode if onlyChanges isn't set,
// which happens e.g. when the node is no longer contributing
// semantics).
if (node._dirty && node.attached)
updatedNodes.add(node._serialize());
}
client.updateSemanticsTree(updatedNodes);
_dirtyNodes.clear();
}
static SemanticActionHandler getSemanticActionHandlerForId(int id, { _SemanticFlags neededFlag }) {
assert(neededFlag != null);
SemanticsNode result = _nodes[id];
if (result != null && result.mergeAllDescendantsIntoThisNode && !result._canHandle(neededFlag)) {
result._visitDescendants((SemanticsNode node) {
if (node._actionHandler != null && node._flags[neededFlag]) {
result = node;
return false; // found node, abort walk
}
return true; // continue walk
});
}
if (result == null || !result._canHandle(neededFlag))
return null;
return result._actionHandler;
}
String toString() {
return '$runtimeType($_id'
'${_dirty ? " (dirty)" : ""}'
'${mergeAllDescendantsIntoThisNode ? " (leaf merge)" : ""}'
'; $rect'
'${wasAffectedByClip ? " (clipped)" : ""}'
'${canBeTapped ? "; canBeTapped" : ""}'
'${canBeLongPressed ? "; canBeLongPressed" : ""}'
'${canBeScrolledHorizontally ? "; canBeScrolledHorizontally" : ""}'
'${canBeScrolledVertically ? "; canBeScrolledVertically" : ""}'
'${hasCheckedState ? (isChecked ? "; checked" : "; unchecked") : ""}'
'${label != "" ? "; \"$label\"" : ""}'
')';
}
String toStringDeep([String prefixLineOne = '', String prefixOtherLines = '']) {
String result = '$prefixLineOne$this\n';
if (_children != null && _children.isNotEmpty) {
for (int index = 0; index < _children.length - 1; index += 1) {
SemanticsNode child = _children[index];
result += '${child.toStringDeep("$prefixOtherLines \u251C", "$prefixOtherLines \u2502")}';
}
result += '${_children.last.toStringDeep("$prefixOtherLines \u2514", "$prefixOtherLines ")}';
}
return result;
}
}
class SemanticsServer extends mojom.SemanticsServer {
void tap(int nodeID) {
SemanticsNode.getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeTapped)?.handleSemanticTap();
}
void longPress(int nodeID) {
SemanticsNode.getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeLongPressed)?.handleSemanticLongPress();
}
void scrollLeft(int nodeID) {
SemanticsNode.getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeScrolledHorizontally)?.handleSemanticScrollLeft();
}
void scrollRight(int nodeID) {
SemanticsNode.getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeScrolledHorizontally)?.handleSemanticScrollRight();
}
void scrollUp(int nodeID) {
SemanticsNode.getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeScrolledVertically)?.handleSemanticScrollUp();
}
void scrollDown(int nodeID) {
SemanticsNode.getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeScrolledVertically)?.handleSemanticScrollDown();
}
}

View file

@ -404,6 +404,8 @@ abstract class RenderStackBase extends RenderBox
paintStack(context, offset);
}
}
Rect describeApproximatePaintClip(RenderObject child) => _hasVisualOverflow ? Point.origin & size : null;
}
/// Implements the stack layout algorithm

View file

@ -141,6 +141,7 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
}
Rect get paintBounds => Point.origin & size;
Rect get semanticBounds => Point.origin & size;
void debugDescribeSettings(List<String> settings) {
// call to ${super.debugDescribeSettings(prefix)} is omitted because the root superclasses don't include any interesting information for this class

View file

@ -55,6 +55,7 @@ class RenderViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox
assert(_offsetIsSane(value, scrollDirection));
_scrollOffset = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
/// The direction in which the child is permitted to be larger than the viewport
@ -137,11 +138,15 @@ class RenderViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox
dyInDevicePixels / devicePixelRatio);
}
bool _wouldNeedClipAtOffset(Offset offset) {
assert(child != null);
return offset < Offset.zero || !(Offset.zero & size).contains(((Offset.zero - offset) & child.size).bottomRight);
}
void paint(PaintingContext context, Offset offset) {
if (child != null) {
Offset roundedScrollOffset = _scrollOffsetRoundedToIntegerDevicePixels;
bool _needsClip = offset < Offset.zero
|| !(offset & size).contains(((offset - roundedScrollOffset) & child.size).bottomRight);
bool _needsClip = _wouldNeedClipAtOffset(roundedScrollOffset);
if (_needsClip) {
context.pushClipRect(needsCompositing, offset, Point.origin & size, (PaintingContext context, Offset offset) {
context.paintChild(child, offset - roundedScrollOffset);
@ -157,6 +162,13 @@ class RenderViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox
super.applyPaintTransform(child, transform);
}
Rect describeApproximatePaintClip(RenderObject child) {
if (child != null &&
_wouldNeedClipAtOffset(_scrollOffsetRoundedToIntegerDevicePixels))
return Point.origin & size;
return null;
}
bool hitTestChildren(HitTestResult result, { Point position }) {
if (child != null) {
assert(child.parentData is BoxParentData);
@ -200,6 +212,7 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende
return;
_paintOffset = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
/// Called during [layout] to determine the grid's children.
@ -254,4 +267,6 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende
void paint(PaintingContext context, Offset offset) {
context.pushClipRect(needsCompositing, offset, Point.origin & size, _paintContents);
}
Rect describeApproximatePaintClip(RenderObject child) => Point.origin & size;
}

View file

@ -2009,21 +2009,116 @@ class RepaintBoundary extends OneChildRenderObjectWidget {
}
class IgnorePointer extends OneChildRenderObjectWidget {
IgnorePointer({ Key key, Widget child, this.ignoring: true })
IgnorePointer({ Key key, Widget child, this.ignoring: true, this.ignoringSemantics })
: super(key: key, child: child);
final bool ignoring;
final bool ignoringSemantics; // if null, defaults to value of ignoring
RenderIgnorePointer createRenderObject() => new RenderIgnorePointer(ignoring: ignoring);
RenderIgnorePointer createRenderObject() => new RenderIgnorePointer(
ignoring: ignoring,
ignoringSemantics: ignoringSemantics
);
void updateRenderObject(RenderIgnorePointer renderObject, IgnorePointer oldWidget) {
renderObject.ignoring = ignoring;
renderObject.ignoringSemantics = ignoringSemantics;
}
}
// UTILITY NODES
/// The Semantics widget annotates the widget tree with a description
/// of the meaning of the widgets, so that accessibility tools, search
/// engines, and other semantic analysis software can determine the
/// meaning of the application.
class Semantics extends OneChildRenderObjectWidget {
Semantics({
Key key,
Widget child,
this.container: false,
this.checked,
this.label
}) : super(key: key, child: child) {
assert(container != null);
}
/// If 'container' is true, this Widget will introduce a new node in
/// the semantics tree. Otherwise, the semantics will be merged with
/// the semantics of any ancestors.
///
/// The 'container' flag is implicitly set to true on the immediate
/// semantics-providing descendants of a node where multiple
/// children have semantics or have descendants providing semantics.
/// In other words, the semantics of siblings are not merged. To
/// merge the semantics of an entire subtree, including siblings,
/// you can use a [MergeSemantics] widget.
final bool container;
/// If non-null, indicates that this subtree represents a checkbox
/// or similar widget with a "checked" state, and what its current
/// state is.
final bool checked;
/// Provides a textual description of the widget.
final String label;
RenderSemanticAnnotations createRenderObject() => new RenderSemanticAnnotations(
container: container,
checked: checked,
label: label
);
void updateRenderObject(RenderSemanticAnnotations renderObject, Semantics oldWidget) {
renderObject.container = container;
renderObject.checked = checked;
renderObject.label = label;
}
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('container: $container');
if (checked != null);
description.add('checked: $checked');
if (label != null);
description.add('label: "$label"');
}
}
/// Causes all the semantics of the subtree rooted at this node to be
/// merged into one node in the semantics tree. For example, if you
/// have a component with a Text node next to a checkbox widget, this
/// could be used to merge the label from the Text node with the
/// "checked" semantic state of the checkbox into a single node that
/// had both the label and the checked state. Otherwise, the label
/// would be presented as a separate feature than the checkbox, and
/// the user would not be able to be sure that they were related.
///
/// Be aware that if two nodes in the subtree have conflicting
/// semantics, the result may be nonsensical. For example, a subtree
/// with a checked checkbox and an unchecked checkbox will be
/// presented as checked. All the labels will be merged into a single
/// string (with newlines separating each label from the other). If
/// multiple nodes in the merged subtree can handle semantic gestures,
/// the first one in tree order will be the one to receive the
/// callbacks.
class MergeSemantics extends OneChildRenderObjectWidget {
MergeSemantics({ Key key, Widget child }) : super(key: key, child: child);
RenderMergeSemantics createRenderObject() => new RenderMergeSemantics();
}
/// Drops all semantics in this subtree.
///
/// This can be used to hide subwidgets that would otherwise be
/// reported but that would only be confusing. For example, the
/// material library's [Chip] widget hides the avatar since it is
/// redundant with the chip label.
class ExcludeSemantics extends OneChildRenderObjectWidget {
ExcludeSemantics({ Key key, Widget child }) : super(key: key, child: child);
RenderExcludeSemantics createRenderObject() => new RenderExcludeSemantics();
}
class MetaData extends OneChildRenderObjectWidget {
MetaData({ Key key, Widget child, this.metaData })
: super(key: key, child: child);

View file

@ -271,13 +271,15 @@ class _FocusState extends State<Focus> {
scheduleMicrotask(_ensureVisibleIfFocused);
}
}
return new _FocusScope(
focusState: this,
scopeFocused: Focus._atScope(context),
focusedScope: _focusedScope == _noFocusedScope ? null : _focusedScope,
focusedWidget: _focusedWidget,
child: config.child
return new Semantics(
container: true,
child: new _FocusScope(
focusState: this,
scopeFocused: Focus._atScope(context),
focusedScope: _focusedScope == _noFocusedScope ? null : _focusedScope,
focusedWidget: _focusedWidget,
child: config.child
)
);
}
}

View file

@ -1466,7 +1466,7 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Buildab
}
void debugUpdateRenderObjectOwner() {
_renderObject.debugOwner = debugGetOwnershipChain(4);
_renderObject.debugOwner = debugGetOwnershipChain(10);
}
void performRebuild() {

View file

@ -31,6 +31,10 @@ export 'package:flutter/gestures.dart' show
///
/// Attempts to recognize gestures that correspond to its non-null callbacks.
///
/// GestureDetector also listens for accessibility events and maps
/// them to the callbacks. To ignore accessibility events, set
/// [excludeFromSemantics] to true.
///
/// See http://flutter.io/gestures/ for additional information.
class GestureDetector extends StatefulComponent {
const GestureDetector({
@ -54,7 +58,8 @@ class GestureDetector extends StatefulComponent {
this.onScaleStart,
this.onScaleUpdate,
this.onScaleEnd,
this.behavior
this.behavior,
this.excludeFromSemantics: false
}) : super(key: key);
final Widget child;
@ -65,7 +70,7 @@ class GestureDetector extends StatefulComponent {
/// A pointer that will trigger a tap has stopped contacting the screen at a
/// particular location.
final GestureTapDownCallback onTapUp;
final GestureTapUpCallback onTapUp;
/// A tap has occurred.
final GestureTapCallback onTap;
@ -117,6 +122,13 @@ class GestureDetector extends StatefulComponent {
/// How this gesture detector should behave during hit testing.
final HitTestBehavior behavior;
/// Whether to exclude these gestures from the semantics tree. For
/// example, the long-press gesture for showing a tooltip is
/// excluded because the tooltip itself is included in the semantics
/// tree directly and so having a gesture to show it would result in
/// duplication of information.
final bool excludeFromSemantics;
_GestureDetectorState createState() => new _GestureDetectorState();
}
@ -269,11 +281,30 @@ class _GestureDetectorState extends State<GestureDetector> {
}
Widget build(BuildContext context) {
return new Listener(
Widget result = new Listener(
onPointerDown: _handlePointerDown,
behavior: config.behavior ?? _defaultBehavior,
child: config.child
);
if (!config.excludeFromSemantics) {
result = new _GestureSemantics(
onTapDown: config.onTapDown,
onTapUp: config.onTapUp,
onTap: config.onTap,
onLongPress: config.onLongPress,
onVerticalDragStart: config.onVerticalDragStart,
onVerticalDragUpdate: config.onVerticalDragUpdate,
onVerticalDragEnd: config.onVerticalDragEnd,
onHorizontalDragStart: config.onHorizontalDragStart,
onHorizontalDragUpdate: config.onHorizontalDragUpdate,
onHorizontalDragEnd: config.onHorizontalDragEnd,
onPanStart: config.onPanStart,
onPanUpdate: config.onPanUpdate,
onPanEnd: config.onPanEnd,
child: result
);
}
return result;
}
void debugFillDescription(List<String> description) {
@ -309,3 +340,122 @@ class _GestureDetectorState extends State<GestureDetector> {
}
}
}
class _GestureSemantics extends OneChildRenderObjectWidget {
_GestureSemantics({
Key key,
this.onTapDown,
this.onTapUp,
this.onTap,
this.onLongPress,
this.onVerticalDragStart,
this.onVerticalDragUpdate,
this.onVerticalDragEnd,
this.onHorizontalDragStart,
this.onHorizontalDragUpdate,
this.onHorizontalDragEnd,
this.onPanStart,
this.onPanUpdate,
this.onPanEnd,
Widget child
}) : super(key: key, child: child);
final GestureTapDownCallback onTapDown;
final GestureTapUpCallback onTapUp;
final GestureTapCallback onTap;
final GestureLongPressCallback onLongPress;
final GestureDragStartCallback onVerticalDragStart;
final GestureDragUpdateCallback onVerticalDragUpdate;
final GestureDragEndCallback onVerticalDragEnd;
final GestureDragStartCallback onHorizontalDragStart;
final GestureDragUpdateCallback onHorizontalDragUpdate;
final GestureDragEndCallback onHorizontalDragEnd;
final GesturePanStartCallback onPanStart;
final GesturePanUpdateCallback onPanUpdate;
final GesturePanEndCallback onPanEnd;
bool get _watchingTaps {
return onTapDown != null
|| onTapUp != null
|| onTap != null;
}
bool get _watchingHorizontalDrags {
return onHorizontalDragStart != null
|| onHorizontalDragUpdate != null
|| onHorizontalDragEnd != null;
}
bool get _watchingVerticalDrags {
return onVerticalDragStart != null
|| onVerticalDragUpdate != null
|| onVerticalDragEnd != null;
}
bool get _watchingPans {
return onPanStart != null
|| onPanUpdate != null
|| onPanEnd != null;
}
void _handleTap() {
if (onTapDown != null)
onTapDown(Point.origin);
if (onTapUp != null)
onTapUp(Point.origin);
if (onTap != null)
onTap();
}
void _handleHorizontalDragUpdate(double delta) {
if (_watchingHorizontalDrags) {
if (onHorizontalDragStart != null)
onHorizontalDragStart(Point.origin);
if (onHorizontalDragUpdate != null)
onHorizontalDragUpdate(delta);
if (onHorizontalDragEnd != null)
onHorizontalDragEnd(Offset.zero);
} else {
assert(_watchingPans);
if (onPanStart != null)
onPanStart(Point.origin);
if (onPanUpdate != null)
onPanUpdate(new Offset(delta, 0.0));
if (onPanEnd != null)
onPanEnd(Offset.zero);
}
}
void _handleVerticalDragUpdate(double delta) {
if (_watchingVerticalDrags) {
if (onVerticalDragStart != null)
onVerticalDragStart(Point.origin);
if (onVerticalDragUpdate != null)
onVerticalDragUpdate(delta);
if (onVerticalDragEnd != null)
onVerticalDragEnd(Offset.zero);
} else {
assert(_watchingPans);
if (onPanStart != null)
onPanStart(Point.origin);
if (onPanUpdate != null)
onPanUpdate(new Offset(0.0, delta));
if (onPanEnd != null)
onPanEnd(Offset.zero);
}
}
RenderSemanticsGestureHandler createRenderObject() => new RenderSemanticsGestureHandler(
onTap: _watchingTaps ? _handleTap : null,
onLongPress: onLongPress,
onHorizontalDragUpdate: _watchingHorizontalDrags || _watchingPans ? _handleHorizontalDragUpdate : null,
onVerticalDragUpdate: _watchingVerticalDrags || _watchingPans ? _handleVerticalDragUpdate : null
);
void updateRenderObject(RenderSemanticsGestureHandler renderObject, _GestureSemantics oldWidget) {
renderObject.onTap = _watchingTaps ? _handleTap : null;
renderObject.onLongPress = onLongPress;
renderObject.onHorizontalDragUpdate = _watchingHorizontalDrags || _watchingPans ? _handleHorizontalDragUpdate : null;
renderObject.onVerticalDragUpdate = _watchingVerticalDrags || _watchingPans ? _handleVerticalDragUpdate : null;
}
}

View file

@ -6,6 +6,7 @@ import 'package:flutter/animation.dart';
import 'basic.dart';
import 'framework.dart';
import 'gesture_detector.dart';
import 'navigator.dart';
import 'transitions.dart';
@ -24,17 +25,20 @@ class ModalBarrier extends StatelessComponent {
final bool dismissable;
Widget build(BuildContext context) {
return new Listener(
onPointerDown: (_) {
if (dismissable)
Navigator.pop(context);
},
behavior: HitTestBehavior.opaque,
child: new ConstrainedBox(
constraints: const BoxConstraints.expand(),
child: color == null ? null : new DecoratedBox(
decoration: new BoxDecoration(
backgroundColor: color
return new Semantics(
container: true,
child: new GestureDetector(
onTapDown: (Point position) {
if (dismissable)
Navigator.pop(context);
},
behavior: HitTestBehavior.opaque,
child: new ConstrainedBox(
constraints: const BoxConstraints.expand(),
child: color == null ? null : new DecoratedBox(
decoration: new BoxDecoration(
backgroundColor: color
)
)
)
)

View file

@ -0,0 +1,362 @@
// Copyright 2015 The Chromium 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:math' as math;
import 'dart:ui' as ui;
import 'package:flutter/painting.dart';
import 'package:flutter/rendering.dart';
import 'package:sky_services/semantics/semantics.mojom.dart' as engine;
import 'basic.dart';
import 'framework.dart';
import 'gesture_detector.dart';
class SemanticsDebugger extends StatefulComponent {
const SemanticsDebugger({ Key key, this.child }) : super(key: key);
final Widget child;
_SemanticsDebuggerState createState() => new _SemanticsDebuggerState();
}
class _SemanticsDebuggerState extends State<SemanticsDebugger> {
void initState() {
super.initState();
_SemanticsDebuggerClient.ensureInstantiated();
_SemanticsDebuggerClient.instance.addListener(_update);
}
void dispose() {
_SemanticsDebuggerClient.instance.removeListener(_update);
super.dispose();
}
void _update() {
setState(() {
// the generation of the _SemanticsDebuggerClient has changed
});
}
Point _lastPointerDownLocation;
void _handlePointerDown(PointerDownEvent event) {
setState(() {
_lastPointerDownLocation = event.position;
});
}
void _handleTap() {
assert(_lastPointerDownLocation != null);
_SemanticsDebuggerClient.instance.handleTap(_lastPointerDownLocation);
setState(() {
_lastPointerDownLocation = null;
});
}
void _handleLongPress() {
assert(_lastPointerDownLocation != null);
_SemanticsDebuggerClient.instance.handleLongPress(_lastPointerDownLocation);
setState(() {
_lastPointerDownLocation = null;
});
}
void _handlePanEnd(Offset velocity) {
assert(_lastPointerDownLocation != null);
_SemanticsDebuggerClient.instance.handlePanEnd(_lastPointerDownLocation, velocity);
setState(() {
_lastPointerDownLocation = null;
});
}
Widget build(BuildContext context) {
return new CustomPaint(
foregroundPainter: new _SemanticsDebuggerPainter(_SemanticsDebuggerClient.instance.generation, _lastPointerDownLocation),
child: new GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _handleTap,
onLongPress: _handleLongPress,
onPanEnd: _handlePanEnd,
excludeFromSemantics: true, // otherwise if you don't hit anything, we end up receiving it, which causes an infinite loop...
child: new Listener(
onPointerDown: _handlePointerDown,
behavior: HitTestBehavior.opaque,
child: new IgnorePointer(
ignoringSemantics: false,
child: config.child
)
)
)
);
}
}
typedef bool _SemanticsDebuggerEntryFilter(_SemanticsDebuggerEntry entry);
class _SemanticsDebuggerEntry {
_SemanticsDebuggerEntry(this.id);
final int id;
bool canBeTapped = false;
bool canBeLongPressed = false;
bool canBeScrolledHorizontally = false;
bool canBeScrolledVertically = false;
bool hasCheckedState = false;
bool isChecked = false;
String label;
Matrix4 transform;
Rect rect;
List<_SemanticsDebuggerEntry> children;
String toString() {
return '_SemanticsDebuggerEntry($id; $rect; "$label"'
'${canBeTapped ? "; canBeTapped" : ""}'
'${canBeLongPressed ? "; canBeLongPressed" : ""}'
'${canBeScrolledHorizontally ? "; canBeScrolledHorizontally" : ""}'
'${canBeScrolledVertically ? "; canBeScrolledVertically" : ""}'
'${hasCheckedState ? isChecked ? "; checked" : "; unchecked" : ""}'
')';
}
String toStringDeep([ String prefix = '']) {
if (prefix.length > 20)
return '$prefix<ABORTED>\n';
String result = '$prefix$this\n';
for (_SemanticsDebuggerEntry child in children.reversed) {
prefix += ' ';
result += '${child.toStringDeep(prefix)}';
}
return result;
}
int findDepth() {
if (children == null || children.isEmpty)
return 1;
return children.map((_SemanticsDebuggerEntry e) => e.findDepth()).reduce(math.max) + 1;
}
static const TextStyle textStyles = const TextStyle(
color: const Color(0xFF000000),
fontSize: 10.0,
height: 0.8,
textAlign: TextAlign.center
);
TextPainter textPainter;
void updateMessage() {
List<String> annotations = <String>[];
bool wantsTap = false;
if (hasCheckedState) {
annotations.add(isChecked ? 'checked' : 'unchecked');
wantsTap = true;
}
if (canBeTapped) {
if (!wantsTap)
annotations.add('button');
} else {
if (wantsTap)
annotations.add('disabled');
}
if (canBeLongPressed)
annotations.add('long-pressable');
if (canBeScrolledHorizontally || canBeScrolledVertically)
annotations.add('scrollable');
String message;
if (annotations.isEmpty) {
assert(label != null);
message = label;
} else {
if (label == '') {
message = annotations.join('; ');
} else {
message = '$label (${annotations.join('; ')})';
}
}
message = message.trim();
if (message != '') {
textPainter ??= new TextPainter();
textPainter.text = new StyledTextSpan(textStyles, <TextSpan>[new PlainTextSpan(message)]);
textPainter.maxWidth = rect.width;
textPainter.maxHeight = rect.height;
textPainter.layout();
} else {
textPainter = null;
}
}
void paint(Canvas canvas, int rank) {
canvas.save();
if (transform != null)
canvas.transform(transform.storage);
if (!rect.isEmpty) {
Color lineColor = new Color(0xFF000000 + new math.Random(id).nextInt(0xFFFFFF));
Rect innerRect = rect.deflate(rank * 1.0);
if (innerRect.isEmpty) {
Paint fill = new Paint()
..color = lineColor
..style = ui.PaintingStyle.fill;
canvas.drawRect(rect, fill);
} else {
Paint fill = new Paint()
..color = const Color(0xFFFFFFFF)
..style = ui.PaintingStyle.fill;
canvas.drawRect(rect, fill);
Paint line = new Paint()
..strokeWidth = rank * 2.0
..color = lineColor
..style = ui.PaintingStyle.stroke;
canvas.drawRect(innerRect, line);
}
if (textPainter != null) {
canvas.save();
canvas.clipRect(rect);
textPainter.paint(canvas, rect.topLeft.toOffset());
canvas.restore();
}
}
for (_SemanticsDebuggerEntry child in children)
child.paint(canvas, rank - 1);
canvas.restore();
}
_SemanticsDebuggerEntry hitTest(Point position, _SemanticsDebuggerEntryFilter filter) {
if (transform != null) {
if (transform.determinant == 0.0)
return null;
Matrix4 invertedTransform = new Matrix4.inverted(transform);
position = MatrixUtils.transformPoint(invertedTransform, position);
}
if (!rect.contains(position))
return null;
_SemanticsDebuggerEntry result;
for (_SemanticsDebuggerEntry child in children.reversed) {
result = child.hitTest(position, filter);
if (result != null)
break;
}
if (result == null || !filter(result))
result = this;
return result;
}
}
class _SemanticsDebuggerClient implements engine.SemanticsClient {
_SemanticsDebuggerClient._() {
Renderer.instance.setSemanticsClient(this);
}
static _SemanticsDebuggerClient instance;
static engine.SemanticsServer _server;
static void ensureInstantiated({ engine.SemanticsServer server }) {
_server = server ?? new SemanticsServer();
instance ??= new _SemanticsDebuggerClient._();
}
Set<VoidCallback> _listeners = new Set<VoidCallback>();
void addListener(VoidCallback callback) {
assert(!_listeners.contains(callback));
_listeners.add(callback);
}
void removeListener(VoidCallback callback) {
_listeners.remove(callback);
}
Map<int, _SemanticsDebuggerEntry> nodes = <int, _SemanticsDebuggerEntry>{};
_SemanticsDebuggerEntry _updateNode(engine.SemanticsNode node) {
_SemanticsDebuggerEntry entry = nodes.putIfAbsent(node.id, () => new _SemanticsDebuggerEntry(node.id));
if (node.flags != null) {
entry.canBeTapped = node.flags.canBeTapped;
entry.canBeLongPressed = node.flags.canBeLongPressed;
entry.canBeScrolledHorizontally = node.flags.canBeScrolledHorizontally;
entry.canBeScrolledVertically = node.flags.canBeScrolledVertically;
entry.hasCheckedState = node.flags.hasCheckedState;
entry.isChecked = node.flags.isChecked;
}
if (node.strings != null) {
assert(node.strings.label != null);
entry.label = node.strings.label;
} else {
assert(entry.label != null);
}
if (node.geometry != null) {
if (node.geometry.transform != null) {
assert(node.geometry.transform.length == 16);
// TODO(ianh): Replace this with a cleaner call once
// https://github.com/google/vector_math.dart/issues/159
// is fixed.
List<double> array = node.geometry.transform;
entry.transform = new Matrix4(
array[0], array[1], array[2], array[3],
array[4], array[5], array[6], array[7],
array[8], array[9], array[10], array[11],
array[12], array[13], array[14], array[15]
);
} else {
entry.transform = null;
}
entry.rect = new Rect.fromLTWH(node.geometry.left, node.geometry.top, node.geometry.width, node.geometry.height);
}
entry.updateMessage();
if (node.children != null) {
Set oldChildren = new Set<_SemanticsDebuggerEntry>.from(entry.children ?? const <_SemanticsDebuggerEntry>[]);
entry.children?.clear();
entry.children ??= new List<_SemanticsDebuggerEntry>();
for (engine.SemanticsNode child in node.children)
entry.children.add(_updateNode(child));
Set newChildren = new Set<_SemanticsDebuggerEntry>.from(entry.children);
Set<_SemanticsDebuggerEntry> removedChildren = oldChildren.difference(newChildren);
for (_SemanticsDebuggerEntry oldChild in removedChildren)
nodes.remove(oldChild.id);
}
return entry;
}
int generation = 0;
updateSemanticsTree(List<engine.SemanticsNode> nodes) {
generation += 1;
for (engine.SemanticsNode node in nodes)
_updateNode(node);
for (VoidCallback listener in _listeners)
listener();
}
_SemanticsDebuggerEntry _hitTest(Point position, _SemanticsDebuggerEntryFilter filter) {
return nodes[0]?.hitTest(position, filter);
}
void handleTap(Point position) {
_server.tap(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeTapped)?.id ?? 0);
}
void handleLongPress(Point position) {
_server.longPress(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeLongPressed)?.id ?? 0);
}
void handlePanEnd(Point position, Offset velocity) {
if (velocity.dx.abs() == velocity.dy.abs())
return;
if (velocity.dx.abs() > velocity.dy.abs()) {
if (velocity.dx.sign < 0)
_server.scrollLeft(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeScrolledHorizontally)?.id ?? 0);
else
_server.scrollRight(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeScrolledHorizontally)?.id ?? 0);
} else {
if (velocity.dy.sign < 0)
_server.scrollUp(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeScrolledVertically)?.id ?? 0);
else
_server.scrollDown(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeScrolledVertically)?.id ?? 0);
}
}
}
class _SemanticsDebuggerPainter extends CustomPainter {
const _SemanticsDebuggerPainter(this.generation, this.pointerPosition);
final int generation;
final Point pointerPosition;
void paint(Canvas canvas, Size size) {
_SemanticsDebuggerClient.instance.nodes[0]?.paint(
canvas,
_SemanticsDebuggerClient.instance.nodes[0].findDepth()
);
if (pointerPosition != null) {
Paint paint = new Paint();
paint.color = const Color(0x7F0090FF);
canvas.drawCircle(pointerPosition, 10.0, paint);
}
}
bool shouldRepaint(_SemanticsDebuggerPainter oldDelegate) {
return generation != oldDelegate.generation
|| pointerPosition != oldDelegate.pointerPosition;
}
}

View file

@ -33,10 +33,11 @@ export 'src/widgets/performance_overlay.dart';
export 'src/widgets/placeholder.dart';
export 'src/widgets/raw_keyboard_listener.dart';
export 'src/widgets/routes.dart';
export 'src/widgets/scroll_behavior.dart';
export 'src/widgets/scrollable.dart';
export 'src/widgets/scrollable_grid.dart';
export 'src/widgets/scrollable_list.dart';
export 'src/widgets/scroll_behavior.dart';
export 'src/widgets/semantics_debugger.dart';
export 'src/widgets/status_transitions.dart';
export 'src/widgets/title.dart';
export 'src/widgets/transitions.dart';

View file

@ -8,8 +8,8 @@ dependencies:
collection: '>=1.1.3 <2.0.0'
intl: '>=0.12.4+2 <0.13.0'
material_design_icons: '>=0.0.3 <0.1.0'
sky_engine: 0.0.87
sky_services: 0.0.87
sky_engine: 0.0.88
sky_services: 0.0.88
vector_math: '>=1.4.5 <2.0.0'
quiver: '>=0.21.4 <0.22.0'

View file

@ -0,0 +1,54 @@
// Copyright 2015 The Chromium 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/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:test/test.dart';
import 'test_semantics.dart';
void main() {
test('Does FlatButton contribute semantics', () {
testWidgets((WidgetTester tester) {
TestSemanticsClient client = new TestSemanticsClient();
tester.pumpWidget(
new Material(
child: new Center(
child: new FlatButton(
onPressed: () { },
child: new Text('Hello')
)
)
)
);
expect(client.updates.length, equals(2));
expect(client.updates[0].id, equals(0));
expect(client.updates[0].flags.canBeTapped, isFalse);
expect(client.updates[0].flags.canBeLongPressed, isFalse);
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
expect(client.updates[0].flags.hasCheckedState, isFalse);
expect(client.updates[0].flags.isChecked, isFalse);
expect(client.updates[0].strings.label, equals(''));
expect(client.updates[0].geometry.transform, isNull);
expect(client.updates[0].geometry.left, equals(0.0));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0));
expect(client.updates[0].geometry.height, equals(600.0));
expect(client.updates[0].children.length, equals(1));
expect(client.updates[0].children[0].id, equals(1));
expect(client.updates[0].children[0].flags.canBeTapped, isTrue);
expect(client.updates[0].children[0].flags.canBeLongPressed, isFalse);
expect(client.updates[0].children[0].flags.canBeScrolledHorizontally, isFalse);
expect(client.updates[0].children[0].flags.canBeScrolledVertically, isFalse);
expect(client.updates[0].children[0].flags.hasCheckedState, isFalse);
expect(client.updates[0].children[0].flags.isChecked, isFalse);
expect(client.updates[0].children[0].strings.label, equals('Hello'));
expect(client.updates[0].children[0].children.length, equals(0));
expect(client.updates[1], isNull);
client.updates.clear();
});
});
}

View file

@ -0,0 +1,250 @@
// Copyright 2015 The Chromium 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';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:test/test.dart';
import 'test_semantics.dart';
void main() {
test('Semantics 1', () {
testWidgets((WidgetTester tester) {
TestSemanticsClient client = new TestSemanticsClient();
// smoketest
tester.pumpWidget(
new Container(
child: new Semantics(
label: 'test1',
child: new Container()
)
)
);
expect(client.updates.length, equals(2));
expect(client.updates[0].id, equals(0));
expect(client.updates[0].flags.canBeTapped, isFalse);
expect(client.updates[0].flags.canBeLongPressed, isFalse);
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
expect(client.updates[0].flags.hasCheckedState, isFalse);
expect(client.updates[0].flags.isChecked, isFalse);
expect(client.updates[0].strings.label, equals('test1'));
expect(client.updates[0].geometry.transform, isNull);
expect(client.updates[0].geometry.left, equals(0.0));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0));
expect(client.updates[0].geometry.height, equals(600.0));
expect(client.updates[0].children.length, equals(0));
expect(client.updates[1], isNull);
client.updates.clear();
// control for forking
tester.pumpWidget(
new Column(
children: <Widget>[
new Container(
height: 10.0,
child: new Semantics(label: 'child1')
),
new Container(
height: 10.0,
child: new IgnorePointer(
ignoring: true,
child: new Semantics(label: 'child2')
)
),
],
alignItems: FlexAlignItems.stretch
)
);
expect(client.updates.length, equals(2));
expect(client.updates[0].id, equals(0));
expect(client.updates[0].flags.canBeTapped, isFalse);
expect(client.updates[0].flags.canBeLongPressed, isFalse);
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
expect(client.updates[0].flags.hasCheckedState, isFalse);
expect(client.updates[0].flags.isChecked, isFalse);
expect(client.updates[0].strings.label, equals('child1'));
expect(client.updates[0].geometry.transform, isNull);
expect(client.updates[0].geometry.left, equals(0.0));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0));
expect(client.updates[0].geometry.height, equals(600.0));
expect(client.updates[0].children.length, equals(0));
expect(client.updates[1], isNull);
client.updates.clear();
// forking semantics
tester.pumpWidget(
new Column(
children: <Widget>[
new Container(
height: 10.0,
child: new Semantics(label: 'child1')
),
new Container(
height: 10.0,
child: new IgnorePointer(
ignoring: false,
child: new Semantics(label: 'child2')
)
),
],
alignItems: FlexAlignItems.stretch
)
);
expect(client.updates.length, equals(2));
expect(client.updates[0].id, equals(0));
expect(client.updates[0].flags.canBeTapped, isFalse);
expect(client.updates[0].flags.canBeLongPressed, isFalse);
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
expect(client.updates[0].flags.hasCheckedState, isFalse);
expect(client.updates[0].flags.isChecked, isFalse);
expect(client.updates[0].strings.label, equals(''));
expect(client.updates[0].geometry.transform, isNull);
expect(client.updates[0].geometry.left, equals(0.0));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0));
expect(client.updates[0].geometry.height, equals(600.0));
expect(client.updates[0].children.length, equals(2));
expect(client.updates[0].children[0].id, equals(1));
expect(client.updates[0].children[0].flags.canBeTapped, isFalse);
expect(client.updates[0].children[0].flags.canBeLongPressed, isFalse);
expect(client.updates[0].children[0].flags.canBeScrolledHorizontally, isFalse);
expect(client.updates[0].children[0].flags.canBeScrolledVertically, isFalse);
expect(client.updates[0].children[0].flags.hasCheckedState, isFalse);
expect(client.updates[0].children[0].flags.isChecked, isFalse);
expect(client.updates[0].children[0].strings.label, equals('child1'));
expect(client.updates[0].children[0].geometry.transform, isNull);
expect(client.updates[0].children[0].geometry.left, equals(0.0));
expect(client.updates[0].children[0].geometry.top, equals(0.0));
expect(client.updates[0].children[0].geometry.width, equals(800.0));
expect(client.updates[0].children[0].geometry.height, equals(10.0));
expect(client.updates[0].children[0].children.length, equals(0));
expect(client.updates[0].children[1].id, equals(2));
expect(client.updates[0].children[1].flags.canBeTapped, isFalse);
expect(client.updates[0].children[1].flags.canBeLongPressed, isFalse);
expect(client.updates[0].children[1].flags.canBeScrolledHorizontally, isFalse);
expect(client.updates[0].children[1].flags.canBeScrolledVertically, isFalse);
expect(client.updates[0].children[1].flags.hasCheckedState, isFalse);
expect(client.updates[0].children[1].flags.isChecked, isFalse);
expect(client.updates[0].children[1].strings.label, equals('child2'));
expect(client.updates[0].children[1].geometry.transform, equals([1.0,0.0,0.0,0.0, 0.0,1.0,0.0,0.0, 0.0,0.0,1.0,0.0, 0.0,10.0,0.0,1.0]));
expect(client.updates[0].children[1].geometry.left, equals(0.0));
expect(client.updates[0].children[1].geometry.top, equals(0.0));
expect(client.updates[0].children[1].geometry.width, equals(800.0));
expect(client.updates[0].children[1].geometry.height, equals(10.0));
expect(client.updates[0].children[1].children.length, equals(0));
expect(client.updates[1], isNull);
client.updates.clear();
// toggle a branch off
tester.pumpWidget(
new Column(
children: <Widget>[
new Container(
height: 10.0,
child: new Semantics(label: 'child1')
),
new Container(
height: 10.0,
child: new IgnorePointer(
ignoring: true,
child: new Semantics(label: 'child2')
)
),
],
alignItems: FlexAlignItems.stretch
)
);
expect(client.updates.length, equals(2));
expect(client.updates[0].id, equals(0));
expect(client.updates[0].flags.canBeTapped, isFalse);
expect(client.updates[0].flags.canBeLongPressed, isFalse);
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
expect(client.updates[0].flags.hasCheckedState, isFalse);
expect(client.updates[0].flags.isChecked, isFalse);
expect(client.updates[0].strings.label, equals('child1'));
expect(client.updates[0].geometry.transform, isNull);
expect(client.updates[0].geometry.left, equals(0.0));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0));
expect(client.updates[0].geometry.height, equals(600.0));
expect(client.updates[0].children.length, equals(0));
expect(client.updates[1], isNull);
client.updates.clear();
// toggle a branch back on
tester.pumpWidget(
new Column(
children: <Widget>[
new Container(
height: 10.0,
child: new Semantics(label: 'child1')
),
new Container(
height: 10.0,
child: new IgnorePointer(
ignoring: false,
child: new Semantics(label: 'child2')
)
),
],
alignItems: FlexAlignItems.stretch
)
);
expect(client.updates.length, equals(2));
expect(client.updates[0].id, equals(0));
expect(client.updates[0].flags.canBeTapped, isFalse);
expect(client.updates[0].flags.canBeLongPressed, isFalse);
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
expect(client.updates[0].flags.hasCheckedState, isFalse);
expect(client.updates[0].flags.isChecked, isFalse);
expect(client.updates[0].strings.label, equals(''));
expect(client.updates[0].geometry.transform, isNull);
expect(client.updates[0].geometry.left, equals(0.0));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0));
expect(client.updates[0].geometry.height, equals(600.0));
expect(client.updates[0].children.length, equals(2));
expect(client.updates[0].children[0].id, equals(3));
expect(client.updates[0].children[0].flags.canBeTapped, isFalse);
expect(client.updates[0].children[0].flags.canBeLongPressed, isFalse);
expect(client.updates[0].children[0].flags.canBeScrolledHorizontally, isFalse);
expect(client.updates[0].children[0].flags.canBeScrolledVertically, isFalse);
expect(client.updates[0].children[0].flags.hasCheckedState, isFalse);
expect(client.updates[0].children[0].flags.isChecked, isFalse);
expect(client.updates[0].children[0].strings.label, equals('child1'));
expect(client.updates[0].children[0].geometry.transform, isNull);
expect(client.updates[0].children[0].geometry.left, equals(0.0));
expect(client.updates[0].children[0].geometry.top, equals(0.0));
expect(client.updates[0].children[0].geometry.width, equals(800.0));
expect(client.updates[0].children[0].geometry.height, equals(10.0));
expect(client.updates[0].children[0].children.length, equals(0));
expect(client.updates[0].children[1].id, equals(2));
expect(client.updates[0].children[1].flags.canBeTapped, isFalse);
expect(client.updates[0].children[1].flags.canBeLongPressed, isFalse);
expect(client.updates[0].children[1].flags.canBeScrolledHorizontally, isFalse);
expect(client.updates[0].children[1].flags.canBeScrolledVertically, isFalse);
expect(client.updates[0].children[1].flags.hasCheckedState, isFalse);
expect(client.updates[0].children[1].flags.isChecked, isFalse);
expect(client.updates[0].children[1].strings.label, equals('child2'));
expect(client.updates[0].children[1].geometry.transform, equals([1.0,0.0,0.0,0.0, 0.0,1.0,0.0,0.0, 0.0,0.0,1.0,0.0, 0.0,10.0,0.0,1.0]));
expect(client.updates[0].children[1].geometry.left, equals(0.0));
expect(client.updates[0].children[1].geometry.top, equals(0.0));
expect(client.updates[0].children[1].geometry.width, equals(800.0));
expect(client.updates[0].children[1].geometry.height, equals(10.0));
expect(client.updates[0].children[1].children.length, equals(0));
expect(client.updates[1], isNull);
client.updates.clear();
});
});
}

View file

@ -0,0 +1,190 @@
// Copyright 2015 The Chromium 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';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:test/test.dart';
import 'test_semantics.dart';
void main() {
test('Semantics 2', () {
testWidgets((WidgetTester tester) {
TestSemanticsClient client = new TestSemanticsClient();
// this test is the same as the test in Semantics 1, but
// starting with the second branch being ignored and then
// switching to not ignoring it.
// forking semantics
tester.pumpWidget(
new Column(
children: <Widget>[
new Container(
height: 10.0,
child: new Semantics(label: 'child1')
),
new Container(
height: 10.0,
child: new IgnorePointer(
ignoring: false,
child: new Semantics(label: 'child2')
)
),
],
alignItems: FlexAlignItems.stretch
)
);
expect(client.updates.length, equals(2));
expect(client.updates[0].id, equals(0));
expect(client.updates[0].flags.canBeTapped, isFalse);
expect(client.updates[0].flags.canBeLongPressed, isFalse);
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
expect(client.updates[0].flags.hasCheckedState, isFalse);
expect(client.updates[0].flags.isChecked, isFalse);
expect(client.updates[0].strings.label, equals(''));
expect(client.updates[0].geometry.transform, isNull);
expect(client.updates[0].geometry.left, equals(0.0));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0));
expect(client.updates[0].geometry.height, equals(600.0));
expect(client.updates[0].children.length, equals(2));
expect(client.updates[0].children[0].id, equals(1));
expect(client.updates[0].children[0].flags.canBeTapped, isFalse);
expect(client.updates[0].children[0].flags.canBeLongPressed, isFalse);
expect(client.updates[0].children[0].flags.canBeScrolledHorizontally, isFalse);
expect(client.updates[0].children[0].flags.canBeScrolledVertically, isFalse);
expect(client.updates[0].children[0].flags.hasCheckedState, isFalse);
expect(client.updates[0].children[0].flags.isChecked, isFalse);
expect(client.updates[0].children[0].strings.label, equals('child1'));
expect(client.updates[0].children[0].geometry.transform, isNull);
expect(client.updates[0].children[0].geometry.left, equals(0.0));
expect(client.updates[0].children[0].geometry.top, equals(0.0));
expect(client.updates[0].children[0].geometry.width, equals(800.0));
expect(client.updates[0].children[0].geometry.height, equals(10.0));
expect(client.updates[0].children[0].children.length, equals(0));
expect(client.updates[0].children[1].id, equals(2));
expect(client.updates[0].children[1].flags.canBeTapped, isFalse);
expect(client.updates[0].children[1].flags.canBeLongPressed, isFalse);
expect(client.updates[0].children[1].flags.canBeScrolledHorizontally, isFalse);
expect(client.updates[0].children[1].flags.canBeScrolledVertically, isFalse);
expect(client.updates[0].children[1].flags.hasCheckedState, isFalse);
expect(client.updates[0].children[1].flags.isChecked, isFalse);
expect(client.updates[0].children[1].strings.label, equals('child2'));
expect(client.updates[0].children[1].geometry.transform, equals([1.0,0.0,0.0,0.0, 0.0,1.0,0.0,0.0, 0.0,0.0,1.0,0.0, 0.0,10.0,0.0,1.0]));
expect(client.updates[0].children[1].geometry.left, equals(0.0));
expect(client.updates[0].children[1].geometry.top, equals(0.0));
expect(client.updates[0].children[1].geometry.width, equals(800.0));
expect(client.updates[0].children[1].geometry.height, equals(10.0));
expect(client.updates[0].children[1].children.length, equals(0));
expect(client.updates[1], isNull);
client.updates.clear();
// toggle a branch off
tester.pumpWidget(
new Column(
children: <Widget>[
new Container(
height: 10.0,
child: new Semantics(label: 'child1')
),
new Container(
height: 10.0,
child: new IgnorePointer(
ignoring: true,
child: new Semantics(label: 'child2')
)
),
],
alignItems: FlexAlignItems.stretch
)
);
expect(client.updates.length, equals(2));
expect(client.updates[0].id, equals(0));
expect(client.updates[0].flags.canBeTapped, isFalse);
expect(client.updates[0].flags.canBeLongPressed, isFalse);
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
expect(client.updates[0].flags.hasCheckedState, isFalse);
expect(client.updates[0].flags.isChecked, isFalse);
expect(client.updates[0].strings.label, equals('child1'));
expect(client.updates[0].geometry.transform, isNull);
expect(client.updates[0].geometry.left, equals(0.0));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0));
expect(client.updates[0].geometry.height, equals(600.0));
expect(client.updates[0].children.length, equals(0));
expect(client.updates[1], isNull);
client.updates.clear();
// toggle a branch back on
tester.pumpWidget(
new Column(
children: <Widget>[
new Container(
height: 10.0,
child: new Semantics(label: 'child1')
),
new Container(
height: 10.0,
child: new IgnorePointer(
ignoring: false,
child: new Semantics(label: 'child2')
)
),
],
alignItems: FlexAlignItems.stretch
)
);
expect(client.updates.length, equals(2));
expect(client.updates[0].id, equals(0));
expect(client.updates[0].flags.canBeTapped, isFalse);
expect(client.updates[0].flags.canBeLongPressed, isFalse);
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
expect(client.updates[0].flags.hasCheckedState, isFalse);
expect(client.updates[0].flags.isChecked, isFalse);
expect(client.updates[0].strings.label, equals(''));
expect(client.updates[0].geometry.transform, isNull);
expect(client.updates[0].geometry.left, equals(0.0));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0));
expect(client.updates[0].geometry.height, equals(600.0));
expect(client.updates[0].children.length, equals(2));
expect(client.updates[0].children[0].id, equals(3));
expect(client.updates[0].children[0].flags.canBeTapped, isFalse);
expect(client.updates[0].children[0].flags.canBeLongPressed, isFalse);
expect(client.updates[0].children[0].flags.canBeScrolledHorizontally, isFalse);
expect(client.updates[0].children[0].flags.canBeScrolledVertically, isFalse);
expect(client.updates[0].children[0].flags.hasCheckedState, isFalse);
expect(client.updates[0].children[0].flags.isChecked, isFalse);
expect(client.updates[0].children[0].strings.label, equals('child1'));
expect(client.updates[0].children[0].geometry.transform, isNull);
expect(client.updates[0].children[0].geometry.left, equals(0.0));
expect(client.updates[0].children[0].geometry.top, equals(0.0));
expect(client.updates[0].children[0].geometry.width, equals(800.0));
expect(client.updates[0].children[0].geometry.height, equals(10.0));
expect(client.updates[0].children[0].children.length, equals(0));
expect(client.updates[0].children[1].id, equals(2));
expect(client.updates[0].children[1].flags.canBeTapped, isFalse);
expect(client.updates[0].children[1].flags.canBeLongPressed, isFalse);
expect(client.updates[0].children[1].flags.canBeScrolledHorizontally, isFalse);
expect(client.updates[0].children[1].flags.canBeScrolledVertically, isFalse);
expect(client.updates[0].children[1].flags.hasCheckedState, isFalse);
expect(client.updates[0].children[1].flags.isChecked, isFalse);
expect(client.updates[0].children[1].strings.label, equals('child2'));
expect(client.updates[0].children[1].geometry.transform, equals([1.0,0.0,0.0,0.0, 0.0,1.0,0.0,0.0, 0.0,0.0,1.0,0.0, 0.0,10.0,0.0,1.0]));
expect(client.updates[0].children[1].geometry.left, equals(0.0));
expect(client.updates[0].children[1].geometry.top, equals(0.0));
expect(client.updates[0].children[1].geometry.width, equals(800.0));
expect(client.updates[0].children[1].geometry.height, equals(10.0));
expect(client.updates[0].children[1].children.length, equals(0));
expect(client.updates[1], isNull);
client.updates.clear();
});
});
}

View file

@ -0,0 +1,152 @@
// Copyright 2015 The Chromium 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/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:test/test.dart';
import 'test_semantics.dart';
void main() {
test('Semantics 3', () {
testWidgets((WidgetTester tester) {
TestSemanticsClient client = new TestSemanticsClient();
// implicit annotators
tester.pumpWidget(
new Container(
child: new Semantics(
label: 'test',
child: new Container(
child: new Semantics(
checked: true
)
)
)
)
);
expect(client.updates.length, equals(2));
expect(client.updates[0].id, equals(0));
expect(client.updates[0].flags.canBeTapped, isFalse);
expect(client.updates[0].flags.canBeLongPressed, isFalse);
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
expect(client.updates[0].flags.hasCheckedState, isTrue);
expect(client.updates[0].flags.isChecked, isTrue);
expect(client.updates[0].strings.label, equals('test'));
expect(client.updates[0].geometry.transform, isNull);
expect(client.updates[0].geometry.left, equals(0.0));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0));
expect(client.updates[0].geometry.height, equals(600.0));
expect(client.updates[0].children.length, equals(0));
expect(client.updates[1], isNull);
client.updates.clear();
// remove one
tester.pumpWidget(
new Container(
child: new Container(
child: new Semantics(
checked: true
)
)
)
);
expect(client.updates.length, equals(2));
expect(client.updates[0].id, equals(0));
expect(client.updates[0].flags.canBeTapped, isFalse);
expect(client.updates[0].flags.canBeLongPressed, isFalse);
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
expect(client.updates[0].flags.hasCheckedState, isTrue);
expect(client.updates[0].flags.isChecked, isTrue);
expect(client.updates[0].strings.label, equals(''));
expect(client.updates[0].geometry.transform, isNull);
expect(client.updates[0].geometry.left, equals(0.0));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0));
expect(client.updates[0].geometry.height, equals(600.0));
expect(client.updates[0].children.length, equals(0));
expect(client.updates[1], isNull);
client.updates.clear();
// change what it says
tester.pumpWidget(
new Container(
child: new Container(
child: new Semantics(
label: 'test'
)
)
)
);
expect(client.updates.length, equals(2));
expect(client.updates[0].id, equals(0));
expect(client.updates[0].flags.canBeTapped, isFalse);
expect(client.updates[0].flags.canBeLongPressed, isFalse);
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
expect(client.updates[0].flags.hasCheckedState, isFalse);
expect(client.updates[0].flags.isChecked, isFalse);
expect(client.updates[0].strings.label, equals('test'));
expect(client.updates[0].geometry.transform, isNull);
expect(client.updates[0].geometry.left, equals(0.0));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0));
expect(client.updates[0].geometry.height, equals(600.0));
expect(client.updates[0].children.length, equals(0));
expect(client.updates[1], isNull);
client.updates.clear();
// add a node
tester.pumpWidget(
new Container(
child: new Semantics(
checked: true,
child: new Container(
child: new Semantics(
label: 'test'
)
)
)
)
);
expect(client.updates.length, equals(2));
expect(client.updates[0].id, equals(0));
expect(client.updates[0].flags.canBeTapped, isFalse);
expect(client.updates[0].flags.canBeLongPressed, isFalse);
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
expect(client.updates[0].flags.hasCheckedState, isTrue);
expect(client.updates[0].flags.isChecked, isTrue);
expect(client.updates[0].strings.label, equals('test'));
expect(client.updates[0].geometry.transform, isNull);
expect(client.updates[0].geometry.left, equals(0.0));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0));
expect(client.updates[0].geometry.height, equals(600.0));
expect(client.updates[0].children.length, equals(0));
expect(client.updates[1], isNull);
client.updates.clear();
// make no changes
tester.pumpWidget(
new Container(
child: new Semantics(
checked: true,
child: new Container(
child: new Semantics(
label: 'test'
)
)
)
)
);
expect(client.updates.length, equals(0));
});
});
}

View file

@ -0,0 +1,120 @@
// Copyright 2015 The Chromium 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/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:test/test.dart';
import 'test_semantics.dart';
void main() {
test('Semantics 4', () {
testWidgets((WidgetTester tester) {
TestSemanticsClient client = new TestSemanticsClient();
// O
// / \ O=root
// L L L=node with label
// / \ C=node with checked
// C C* *=node removed next pass
//
tester.pumpWidget(
new Stack(
children: <Widget>[
new Semantics(
label: 'L1'
),
new Semantics(
label: 'L2',
child: new Stack(
children: <Widget>[
new Semantics(
checked: true
),
new Semantics(
checked: false
)
]
)
)
]
)
);
expect(client.updates.length, equals(2));
expect(client.updates[0].id, equals(0));
expect(client.updates[0].children.length, equals(2));
expect(client.updates[0].children[0].id, equals(1));
expect(client.updates[0].children[0].children.length, equals(0));
expect(client.updates[0].children[1].id, equals(2));
expect(client.updates[0].children[1].children.length, equals(2));
expect(client.updates[0].children[1].children[0].id, equals(3));
expect(client.updates[0].children[1].children[0].children.length, equals(0));
expect(client.updates[0].children[1].children[1].id, equals(4));
expect(client.updates[0].children[1].children[1].children.length, equals(0));
expect(client.updates[1], isNull);
client.updates.clear();
// O O=root
// / \ L=node with label
// L* LC C=node with checked
// *=node removed next pass
//
tester.pumpWidget(
new Stack(
children: <Widget>[
new Semantics(
label: 'L1'
),
new Semantics(
label: 'L2',
child: new Stack(
children: <Widget>[
new Semantics(
checked: true
),
new Semantics()
]
)
)
]
)
);
expect(client.updates.length, equals(2));
expect(client.updates[0].id, equals(2));
expect(client.updates[0].children.length, equals(0));
expect(client.updates[1], isNull);
client.updates.clear();
// O=root
// OLC L=node with label
// C=node with checked
//
tester.pumpWidget(
new Stack(
children: <Widget>[
new Semantics(),
new Semantics(
label: 'L2',
child: new Stack(
children: <Widget>[
new Semantics(
checked: true
),
new Semantics()
]
)
)
]
)
);
expect(client.updates.length, equals(2));
expect(client.updates[0].id, equals(0));
expect(client.updates[0].children.length, equals(0));
expect(client.updates[1], isNull);
client.updates.clear();
});
});
}

View file

@ -0,0 +1,49 @@
// Copyright 2015 The Chromium 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/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:test/test.dart';
import 'test_semantics.dart';
void main() {
test('Semantics 5', () {
testWidgets((WidgetTester tester) {
TestSemanticsClient client = new TestSemanticsClient();
tester.pumpWidget(
new Stack(
children: <Widget>[
new Semantics(
// this tests that empty nodes disappear
),
new Semantics(
// this tests whether you can have a container with no other semantics
container: true
),
new Semantics(
label: 'label' // (force a fork)
),
]
)
);
expect(client.updates.length, equals(2));
expect(client.updates[0].id, equals(0));
expect(client.updates[0].flags.hasCheckedState, isFalse);
expect(client.updates[0].strings.label, equals(''));
expect(client.updates[0].children.length, equals(2));
expect(client.updates[0].children[0].id, equals(1));
expect(client.updates[0].children[0].flags.hasCheckedState, isFalse);
expect(client.updates[0].children[0].strings.label, equals(''));
expect(client.updates[0].children[1].id, equals(2));
expect(client.updates[0].children[1].flags.hasCheckedState, isFalse);
expect(client.updates[0].children[1].strings.label, equals('label'));
expect(client.updates[1], isNull);
client.updates.clear();
});
});
}

View file

@ -0,0 +1,50 @@
// Copyright 2015 The Chromium 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/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:test/test.dart';
void main() {
test('Semantics 6 - SemanticsDebugger smoke test', () {
testWidgets((WidgetTester tester) {
// This is a smoketest to verify that adding a debugger doesn't crash.
tester.pumpWidget(
new Stack(
children: <Widget>[
new Semantics(),
new Semantics(
container: true
),
new Semantics(
label: 'label'
),
]
)
);
tester.pumpWidget(
new SemanticsDebugger(
child: new Stack(
children: <Widget>[
new Semantics(),
new Semantics(
container: true
),
new Semantics(
label: 'label'
),
]
)
)
);
expect(true, isTrue); // expect that we reach here without crashing
});
});
}

View file

@ -0,0 +1,18 @@
// Copyright 2015 The Chromium 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/rendering.dart';
import 'package:sky_services/semantics/semantics.mojom.dart' as engine;
class TestSemanticsClient implements engine.SemanticsClient {
TestSemanticsClient() {
Renderer.instance.setSemanticsClient(this);
}
final List<engine.SemanticsNode> updates = <engine.SemanticsNode>[];
updateSemanticsTree(List<engine.SemanticsNode> nodes) {
assert(!nodes.any((engine.SemanticsNode node) => node == null));
updates.addAll(nodes);
updates.add(null);
}
}

View file

@ -8,6 +8,8 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:test/test.dart';
import 'test_semantics.dart';
void main() {
test('Does tooltip end up in the right place - top left', () {
testWidgets((WidgetTester tester) {
@ -354,4 +356,74 @@ void main() {
expect(tip.localToGlobal(tip.size.bottomRight(Point.origin)).y, equals(320.0));
});
});
test('Does tooltip contribute semantics', () {
testWidgets((WidgetTester tester) {
TestSemanticsClient client = new TestSemanticsClient();
GlobalKey key = new GlobalKey();
tester.pumpWidget(
new Overlay(
initialEntries: <OverlayEntry>[
new OverlayEntry(
builder: (BuildContext context) {
return new Stack(
children: <Widget>[
new Positioned(
left: 780.0,
top: 300.0,
child: new Tooltip(
key: key,
message: 'TIP',
fadeDuration: const Duration(seconds: 1),
showDuration: const Duration(seconds: 2),
child: new Container(width: 0.0, height: 0.0)
)
),
]
);
}
),
]
)
);
expect(client.updates.length, equals(2));
expect(client.updates[0].id, equals(0));
expect(client.updates[0].flags.canBeTapped, isFalse);
expect(client.updates[0].flags.canBeLongPressed, isFalse);
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
expect(client.updates[0].flags.hasCheckedState, isFalse);
expect(client.updates[0].flags.isChecked, isFalse);
expect(client.updates[0].strings.label, equals('TIP'));
expect(client.updates[0].geometry.transform, isNull);
expect(client.updates[0].geometry.left, equals(0.0));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0));
expect(client.updates[0].geometry.height, equals(600.0));
expect(client.updates[0].children.length, equals(0));
expect(client.updates[1], isNull);
client.updates.clear();
key.currentState.showTooltip(); // this triggers a rebuild of the semantics because the tree changes
tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
expect(client.updates.length, equals(2));
expect(client.updates[0].id, equals(0));
expect(client.updates[0].flags.canBeTapped, isFalse);
expect(client.updates[0].flags.canBeLongPressed, isFalse);
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
expect(client.updates[0].flags.hasCheckedState, isFalse);
expect(client.updates[0].flags.isChecked, isFalse);
expect(client.updates[0].strings.label, equals('TIP'));
expect(client.updates[0].geometry.transform, isNull);
expect(client.updates[0].geometry.left, equals(0.0));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0));
expect(client.updates[0].geometry.height, equals(600.0));
expect(client.updates[0].children.length, equals(0));
expect(client.updates[1], isNull);
client.updates.clear();
});
});
}