Chromium Code Reviews| Index: sky/sdk/lib/widgets/tabs.dart |
| diff --git a/sky/sdk/lib/widgets/tabs.dart b/sky/sdk/lib/widgets/tabs.dart |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..2dcec21919b7b971406c0b5f4b5bca62b338eb94 |
| --- /dev/null |
| +++ b/sky/sdk/lib/widgets/tabs.dart |
| @@ -0,0 +1,267 @@ |
| +// 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:sky' as sky; |
| + |
| +import 'package:sky/painting/text_style.dart'; |
| +import 'package:sky/rendering/box.dart'; |
| +import 'package:sky/rendering/object.dart'; |
| +import 'package:sky/theme/colors.dart'; |
| +import 'package:sky/widgets/basic.dart'; |
| +import 'package:sky/widgets/icon.dart'; |
| +import 'package:sky/widgets/ink_well.dart'; |
| +import 'package:sky/widgets/widget.dart'; |
| + |
| +typedef void SelectedIndexChanged(int selectedIndex); |
| + |
| +const double _kTabHeight = 46.0; |
| +const double _kTabIndicatorHeight = 2.0; |
| +const double _kTabBarHeight = _kTabHeight + _kTabIndicatorHeight; |
| +const double _kMinTabWidth = 72.0; |
| + |
| +class TabBarParentData extends BoxParentData with |
| + ContainerParentDataMixin<RenderBox> { } |
| + |
| +class RenderTabBar extends RenderBox with |
| + ContainerRenderObjectMixin<RenderBox, TabBarParentData>, |
| + RenderBoxContainerDefaultsMixin<RenderBox, TabBarParentData> { |
| + |
| + int _selectedIndex; |
| + int get selectedIndex => _selectedIndex; |
| + void set selectedIndex(int value) { |
| + if (_selectedIndex != value) { |
| + _selectedIndex = value; |
| + markNeedsPaint(); |
| + } |
| + } |
| + |
| + void setParentData(RenderBox child) { |
| + if (child.parentData is! TabBarParentData) { |
| + child.parentData = new TabBarParentData(); |
| + } |
| + } |
| + |
| + double getMinIntrinsicWidth(BoxConstraints constraints) { |
| + BoxConstraints widthConstraints = constraints.widthConstraints(); |
| + double maxWidth = 0.0; |
| + int childCount = 0; |
| + for (RenderBox child = firstChild; child != null; child = child.parentData.nextSibling) { |
| + maxWidth = math.max(maxWidth, child.getMinIntrinsicWidth(widthConstraints)); |
| + ++childCount; |
| + } |
| + return maxWidth * childCount.toDouble(); |
| + } |
| + |
| + double getMaxIntrinsicWidth(BoxConstraints constraints) { |
| + BoxConstraints widthConstraints = constraints.widthConstraints(); |
| + double maxWidth = 0.0; |
| + int childCount = 0; |
| + for (RenderBox child = firstChild; child != null; child = child.parentData.nextSibling) { |
| + maxWidth = math.max(maxWidth, child.getMaxIntrinsicWidth(widthConstraints)); |
| + ++childCount; |
| + } |
| + return maxWidth * childCount.toDouble(); |
| + } |
| + |
| + double _getIntrinsicHeight(BoxConstraints constraints) => constraints.constrainHeight(_kTabBarHeight); |
| + |
| + double getMinIntrinsicHeight(BoxConstraints constraints) => _getIntrinsicHeight(constraints); |
| + |
| + double getMaxIntrinsicHeight(BoxConstraints constraints) => _getIntrinsicHeight(constraints); |
| + |
| + int _childCount() { |
| + int childCount = 0; |
| + for (RenderBox child = firstChild; child != null; child = child.parentData.nextSibling) { |
| + ++childCount; |
| + } |
| + return childCount; |
| + } |
| + |
| + void performLayout() { |
| + assert(constraints is BoxConstraints); |
| + |
| + size = constraints.constrain(new Size(constraints.maxWidth, _kTabBarHeight)); |
| + assert(size.width < double.INFINITY); |
| + assert(size.height < double.INFINITY); |
| + |
| + int childCount = _childCount(); |
| + if (childCount == 0) return; |
| + |
| + double tabWidth = constraints.constrainWidth(constraints.maxWidth) / childCount.toDouble(); |
| + BoxConstraints tabConstraints = |
| + new BoxConstraints.tightFor(width: tabWidth, height: size.height); |
| + double x = 0.0; |
| + for (RenderBox child = firstChild; child != null; child = child.parentData.nextSibling) { |
| + child.layout(tabConstraints, parentUsesSize: true); |
|
abarth-chromium
2015/06/25 18:25:49
Rather than passing |parentUsesSize: true| here, w
hansmuller
2015/06/25 19:45:50
Sorry, I wasn't disagreeing with this feedback; It
|
| + assert(child.parentData is TabBarParentData); |
| + child.parentData.position = new Point(x, 0.0); |
| + x += child.size.width; |
| + } |
| + } |
| + |
| + void hitTestChildren(HitTestResult result, { Point position }) { |
| + defaultHitTestChildren(result, position: position); |
| + } |
| + |
| + void _paintBackground(RenderObjectDisplayList canvas) { |
| + // TODO(hansmuller): background color should be based on the theme. |
| + Paint paint = new Paint() |
| + ..color = Blue[500] |
| + ..setStyle(sky.PaintingStyle.fill); |
| + Rect rect = new Rect.fromSize(size); |
| + canvas.drawRect(rect, paint); |
| + } |
| + |
| + void _paintIndicator(RenderObjectDisplayList canvas, RenderBox selectedTab) { |
| + var size = new Size(selectedTab.size.width, 2.0); |
| + var point = new Point(selectedTab.parentData.position.x, 46.0); |
| + Rect rect = new Rect.fromPointAndSize(point, size); |
| + // TODO(hansmuller): indicator color should be based on the theme. |
| + Paint paint = new Paint() |
| + ..color = White |
| + ..setStyle(sky.PaintingStyle.fill); |
| + canvas.drawRect(rect, paint); |
| + } |
| + |
| + void paint(RenderObjectDisplayList canvas) { |
| + _paintBackground(canvas); |
| + |
| + var index = 0; |
| + for (RenderBox child = firstChild; child != null; child = child.parentData.nextSibling) { |
| + assert(child.parentData is TabBarParentData); |
| + canvas.paintChild(child, child.parentData.position); |
| + if (index++ == selectedIndex) _paintIndicator(canvas, child); |
| + } |
| + } |
| +} |
| + |
| +class TabBarWrapper extends MultiChildRenderObjectWrapper { |
| + TabBarWrapper(List<Widget> children, int this.selectedIndex, { String key }) |
| + : super(key: key, children: children); |
| + |
| + final int selectedIndex; |
| + |
| + RenderTabBar get root => super.root; |
| + RenderTabBar createNode() => new RenderTabBar(); |
| + |
| + void syncRenderObject(Widget old) { |
| + super.syncRenderObject(old); |
| + root.selectedIndex = selectedIndex; |
| + } |
| +} |
| + |
| +class TabLabel { |
| + const TabLabel({ this.text, this.icon }); |
| + |
| + final String text; |
| + final String icon; |
| +} |
| + |
| +class Tab extends Component { |
| + Tab({ |
| + String key, |
| + this.label, |
| + this.selected: false |
| + }) : super(key: key) { |
| + assert(label.text != null || label.icon != null); |
| + } |
| + |
| + final TabLabel label; |
| + final bool selected; |
| + |
| + // TODO(hansmuller): use themes here. |
| + static const TextStyle selectedStyle = const TextStyle(color: const Color(0xFFFFFFFF)); |
| + static const TextStyle style = const TextStyle(color: const Color(0xB2FFFFFF)); |
| + |
| + Widget _buildLabelText() { |
| + assert(label.text != null); |
| + return new Text(label.text, style: style); |
| + } |
| + |
| + Widget _buildLabelIcon() { |
| + assert(label.icon != null); |
| + return new Icon(type: label.icon, size: 24); |
| + } |
| + |
| + Widget build() { |
| + Widget labelContents; |
| + if (label.icon == null) { |
| + labelContents = _buildLabelText(); |
| + } else if (label.text == null) { |
| + labelContents = _buildLabelIcon(); |
| + } else { |
| + labelContents = new Flex( |
| + <Widget>[_buildLabelText(), _buildLabelIcon()], |
| + justifyContent: FlexJustifyContent.center, |
| + alignItems: FlexAlignItems.center, |
| + direction: FlexDirection.vertical |
| + ); |
| + } |
| + |
| + Widget highlightedLabel = new Opacity( |
| + child: labelContents, |
| + opacity: selected ? 1.0 : 0.7 |
| + ); |
| + |
| + Container centeredLabel = new Container( |
| + child: new Center(child: highlightedLabel), |
| + constraints: new BoxConstraints(minWidth: _kMinTabWidth) |
| + ); |
| + |
| + return new InkWell(child: centeredLabel); |
| + } |
| +} |
| + |
| +class TabBar extends Component { |
| + TabBar({ |
| + String key, |
| + this.labels, |
| + this.selectedIndex: 0, |
| + this.onChanged |
| + }) : super(key: key, stateful: true); |
| + |
| + List<TabLabel> labels; |
| + int selectedIndex; |
| + SelectedIndexChanged onChanged; |
| + |
| + void syncFields(TabBar source) { |
| + labels = source.labels; |
| + selectedIndex = source.selectedIndex; |
| + onChanged = source.onChanged; |
| + super.syncFields(source); |
| + } |
| + |
| + void _handleTap(int tabIndex) { |
| + if (tabIndex != selectedIndex) { |
| + setState(() { |
| + selectedIndex = tabIndex; |
| + }); |
| + if (onChanged != null) onChanged(selectedIndex); |
| + } |
| + } |
| + |
| + Widget _toTab(TabLabel label, int tabIndex) { |
| + Tab tab = new Tab( |
| + label: label, |
| + selected: tabIndex == selectedIndex, |
| + key: label.text == null ? label.icon : label.text |
| + ); |
| + return new Listener( |
| + child: tab, |
| + onGestureTap: (_) => _handleTap(tabIndex) |
| + ); |
| + } |
| + |
| + Widget build() { |
| + assert(labels != null && labels.isNotEmpty); |
| + List<Widget> tabs = <Widget>[]; |
| + for (int tabIndex = 0; tabIndex < labels.length; tabIndex++) { |
| + tabs.add(_toTab(labels[tabIndex], tabIndex)); |
| + } |
| + return new TabBarWrapper(tabs, selectedIndex); |
| + } |
| +} |
| + |
| + |