Commit 28a17883 authored by Hixie's avatar Hixie

Semantics

parent b5470df8
......@@ -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'
)
......
......@@ -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 ||
......
......@@ -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
......
......@@ -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)
......
......@@ -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(),
......
......@@ -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
......
......@@ -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
);
}
}
\ No newline at end of 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';
......
......@@ -31,7 +31,7 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
);
GestureTapDownCallback onTapDown;
GestureTapDownCallback onTapUp;
GestureTapUpCallback onTapUp;
GestureTapCallback onTap;
GestureTapCancelCallback onTapCancel;
......
......@@ -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
)
)
);
}
......
......@@ -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,
......
......@@ -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)
)
)
);
}
......
......@@ -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) {
......
......@@ -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;
}
......
......@@ -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
)
)
)
);
......
......@@ -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
)
);
}
}
......
......@@ -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
)
)
);
}
......
......@@ -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
)
)
)
)
......
......@@ -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
)
);
}
);
}
}
......@@ -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() { }
}
......@@ -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
)
);
}
}
......
......@@ -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;
}
......
......@@ -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)
);
}
}
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.
......@@ -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;
}
}
......@@ -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 {
......
......@@ -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));
......
......@@ -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.
......
......@@ -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.
......
......@@ -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;
}
......@@ -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)
......
......@@ -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) { }
}
......@@ -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'
......
This diff is collapsed.
......@@ -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
......
......@@ -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
......
......@@ -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;
}
......@@ -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);
......
......@@ -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
)
);
}
}
......@@ -1466,7 +1466,7 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Buildab
}
void debugUpdateRenderObjectOwner() {
_renderObject.debugOwner = debugGetOwnershipChain(4);
_renderObject.debugOwner = debugGetOwnershipChain(10);
}
void performRebuild() {
......
......@@ -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;
}
}
......@@ -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
)
)
)
)
......
This diff is collapsed.
......@@ -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';
......
......@@ -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'
......
// 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();
});
});
}
This diff is collapsed.
// 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();
});
});
}
// 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));
});
});
}
// 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();
});
});
}
// 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();
});
});
}
// 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
});
});
}
// 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);
}
}
......@@ -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();
});
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment