Commit 0a0a92eb authored by Hixie's avatar Hixie

Port stocks to fn3 and introduce an App component.

parent 93f1ba5d
...@@ -10,7 +10,8 @@ import 'dart:sky' as sky; ...@@ -10,7 +10,8 @@ import 'dart:sky' as sky;
import 'package:sky/animation.dart'; import 'package:sky/animation.dart';
import 'package:sky/material.dart'; import 'package:sky/material.dart';
import 'package:sky/widgets.dart'; import 'package:sky/painting.dart';
import 'package:sky/src/fn3.dart';
import 'stock_data.dart'; import 'stock_data.dart';
...@@ -22,86 +23,64 @@ part 'stock_row.dart'; ...@@ -22,86 +23,64 @@ part 'stock_row.dart';
part 'stock_settings.dart'; part 'stock_settings.dart';
part 'stock_types.dart'; part 'stock_types.dart';
class StocksApp extends App { class StocksApp extends StatefulComponent {
StocksAppState createState() => new StocksAppState();
}
NavigationState _navigationState; class StocksAppState extends State<StocksApp> {
void initState() { final List<Stock> _stocks = [];
_navigationState = new NavigationState([
new Route(
name: '/',
builder: (navigator, route) => new StockHome(navigator, _stocks, optimismSetting, modeUpdater)
),
new Route(
name: '/settings',
builder: (navigator, route) => new StockSettings(navigator, optimismSetting, backupSetting, settingsUpdater)
),
]);
super.initState();
}
void onBack() { void initState(BuildContext context) {
if (_navigationState.hasPrevious()) { super.initState(context);
new StockDataFetcher((StockData data) {
setState(() { setState(() {
_navigationState.pop(); data.appendTo(_stocks);
}); });
} else { });
super.onBack();
}
} }
StockMode optimismSetting = StockMode.optimistic; StockMode _optimismSetting = StockMode.optimistic;
BackupMode backupSetting = BackupMode.disabled; BackupMode _backupSetting = BackupMode.disabled;
void modeUpdater(StockMode optimism) { void modeUpdater(StockMode optimism) {
setState(() { setState(() {
optimismSetting = optimism; _optimismSetting = optimism;
}); });
} }
void settingsUpdater({ StockMode optimism, BackupMode backup }) { void settingsUpdater({ StockMode optimism, BackupMode backup }) {
setState(() { setState(() {
if (optimism != null) if (optimism != null)
optimismSetting = optimism; _optimismSetting = optimism;
if (backup != null) if (backup != null)
backupSetting = backup; _backupSetting = backup;
});
}
final List<Stock> _stocks = [];
void didMount() {
super.didMount();
new StockDataFetcher((StockData data) {
setState(() {
data.appendTo(_stocks);
});
}); });
} }
Widget build() { ThemeData get theme {
switch (_optimismSetting) {
ThemeData theme; case StockMode.optimistic:
if (optimismSetting == StockMode.optimistic) { return new ThemeData(
theme = new ThemeData( brightness: ThemeBrightness.light,
brightness: ThemeBrightness.light, primarySwatch: Colors.purple
primarySwatch: Colors.purple );
); case StockMode.pessimistic:
} else { return new ThemeData(
theme = new ThemeData( brightness: ThemeBrightness.dark,
brightness: ThemeBrightness.dark, accentColor: Colors.redAccent[200]
accentColor: Colors.redAccent[200] );
);
} }
}
return new Theme( Widget build(BuildContext context) {
data: theme, return new App(
child: new DefaultTextStyle( title: 'Stocks',
style: Typography.error, // if you see this, you've forgotten to correctly configure the text style! theme: theme,
child: new Title( routes: <String, RouteBuilder>{
title: 'Stocks', '/': (navigator, route) => new StockHome(navigator, _stocks, _optimismSetting, modeUpdater),
child: new Navigator(_navigationState) '/settings': (navigator, route) => new StockSettings(navigator, _optimismSetting, _backupSetting, settingsUpdater)
) }
) );
); }
}
} }
void main() { void main() {
......
...@@ -4,8 +4,7 @@ ...@@ -4,8 +4,7 @@
part of stocks; part of stocks;
class StockArrow extends Component { class StockArrow extends StatelessComponent {
StockArrow({ Key key, this.percentChange }) : super(key: key); StockArrow({ Key key, this.percentChange }) : super(key: key);
final double percentChange; final double percentChange;
...@@ -22,7 +21,7 @@ class StockArrow extends Component { ...@@ -22,7 +21,7 @@ class StockArrow extends Component {
return Colors.red[_colorIndexForPercentChange(percentChange)]; return Colors.red[_colorIndexForPercentChange(percentChange)];
} }
Widget build() { Widget build(BuildContext context) {
// TODO(jackson): This should change colors with the theme // TODO(jackson): This should change colors with the theme
Color color = _colorForPercentChange(percentChange); Color color = _colorForPercentChange(percentChange);
const double kSize = 40.0; const double kSize = 40.0;
...@@ -65,5 +64,4 @@ class StockArrow extends Component { ...@@ -65,5 +64,4 @@ class StockArrow extends Component {
margin: const EdgeDims.symmetric(horizontal: 5.0) margin: const EdgeDims.symmetric(horizontal: 5.0)
); );
} }
} }
...@@ -9,20 +9,17 @@ typedef void ModeUpdater(StockMode mode); ...@@ -9,20 +9,17 @@ typedef void ModeUpdater(StockMode mode);
const Duration _kSnackbarSlideDuration = const Duration(milliseconds: 200); const Duration _kSnackbarSlideDuration = const Duration(milliseconds: 200);
class StockHome extends StatefulComponent { class StockHome extends StatefulComponent {
StockHome(this.navigator, this.stocks, this.stockMode, this.modeUpdater); StockHome(this.navigator, this.stocks, this.stockMode, this.modeUpdater);
Navigator navigator; final NavigatorState navigator;
List<Stock> stocks; final List<Stock> stocks;
StockMode stockMode; final StockMode stockMode;
ModeUpdater modeUpdater; final ModeUpdater modeUpdater;
void syncConstructorArguments(StockHome source) { StockHomeState createState() => new StockHomeState();
navigator = source.navigator; }
stocks = source.stocks;
stockMode = source.stockMode; class StockHomeState extends State<StockHome> {
modeUpdater = source.modeUpdater;
}
bool _isSearching = false; bool _isSearching = false;
String _searchQuery; String _searchQuery;
...@@ -31,7 +28,7 @@ class StockHome extends StatefulComponent { ...@@ -31,7 +28,7 @@ class StockHome extends StatefulComponent {
bool _isSnackBarShowing = false; bool _isSnackBarShowing = false;
void _handleSearchBegin() { void _handleSearchBegin() {
navigator.pushState(this, (_) { config.navigator.pushState(this, (_) {
setState(() { setState(() {
_isSearching = false; _isSearching = false;
_searchQuery = null; _searchQuery = null;
...@@ -43,9 +40,9 @@ class StockHome extends StatefulComponent { ...@@ -43,9 +40,9 @@ class StockHome extends StatefulComponent {
} }
void _handleSearchEnd() { void _handleSearchEnd() {
assert(navigator.currentRoute is RouteState); assert(config.navigator.currentRoute is RouteState);
assert((navigator.currentRoute as RouteState).owner == this); // TODO(ianh): remove cast once analyzer is cleverer assert((config.navigator.currentRoute as RouteState).owner == this); // TODO(ianh): remove cast once analyzer is cleverer
navigator.pop(); config.navigator.pop();
setState(() { setState(() {
_isSearching = false; _isSearching = false;
_searchQuery = null; _searchQuery = null;
...@@ -82,15 +79,12 @@ class StockHome extends StatefulComponent { ...@@ -82,15 +79,12 @@ class StockHome extends StatefulComponent {
} }
void _handleStockModeChange(StockMode value) { void _handleStockModeChange(StockMode value) {
setState(() { if (config.modeUpdater != null)
stockMode = value; config.modeUpdater(value);
});
if (modeUpdater != null)
modeUpdater(value);
} }
void _handleMenuShow() { void _handleMenuShow() {
showStockMenu(navigator, showStockMenu(config.navigator,
autorefresh: _autorefresh, autorefresh: _autorefresh,
onAutorefreshChanged: _handleAutorefreshChanged onAutorefreshChanged: _handleAutorefreshChanged
); );
...@@ -104,7 +98,7 @@ class StockHome extends StatefulComponent { ...@@ -104,7 +98,7 @@ class StockHome extends StatefulComponent {
level: 3, level: 3,
showing: _drawerShowing, showing: _drawerShowing,
onDismissed: _handleDrawerDismissed, onDismissed: _handleDrawerDismissed,
navigator: navigator, navigator: config.navigator,
children: [ children: [
new DrawerHeader(child: new Text('Stocks')), new DrawerHeader(child: new Text('Stocks')),
new DrawerItem( new DrawerItem(
...@@ -122,7 +116,7 @@ class StockHome extends StatefulComponent { ...@@ -122,7 +116,7 @@ class StockHome extends StatefulComponent {
onPressed: () => _handleStockModeChange(StockMode.optimistic), onPressed: () => _handleStockModeChange(StockMode.optimistic),
child: new Row([ child: new Row([
new Flexible(child: new Text('Optimistic')), new Flexible(child: new Text('Optimistic')),
new Radio(value: StockMode.optimistic, groupValue: stockMode, onChanged: _handleStockModeChange) new Radio(value: StockMode.optimistic, groupValue: config.stockMode, onChanged: _handleStockModeChange)
]) ])
), ),
new DrawerItem( new DrawerItem(
...@@ -130,7 +124,7 @@ class StockHome extends StatefulComponent { ...@@ -130,7 +124,7 @@ class StockHome extends StatefulComponent {
onPressed: () => _handleStockModeChange(StockMode.pessimistic), onPressed: () => _handleStockModeChange(StockMode.pessimistic),
child: new Row([ child: new Row([
new Flexible(child: new Text('Pessimistic')), new Flexible(child: new Text('Pessimistic')),
new Radio(value: StockMode.pessimistic, groupValue: stockMode, onChanged: _handleStockModeChange) new Radio(value: StockMode.pessimistic, groupValue: config.stockMode, onChanged: _handleStockModeChange)
]) ])
), ),
new DrawerDivider(), new DrawerDivider(),
...@@ -146,23 +140,26 @@ class StockHome extends StatefulComponent { ...@@ -146,23 +140,26 @@ class StockHome extends StatefulComponent {
} }
void _handleShowSettings() { void _handleShowSettings() {
navigator.pop(); config.navigator.pop();
navigator.pushNamed('/settings'); config.navigator.pushNamed('/settings');
} }
Widget buildToolBar() { Widget buildToolBar() {
return new ToolBar( return new ToolBar(
left: new IconButton( left: new IconButton(
icon: "navigation/menu", icon: "navigation/menu",
onPressed: _handleOpenDrawer), onPressed: _handleOpenDrawer
),
center: new Text('Stocks'), center: new Text('Stocks'),
right: [ right: [
new IconButton( new IconButton(
icon: "action/search", icon: "action/search",
onPressed: _handleSearchBegin), onPressed: _handleSearchBegin
),
new IconButton( new IconButton(
icon: "navigation/more_vert", icon: "navigation/more_vert",
onPressed: _handleMenuShow) onPressed: _handleMenuShow
)
] ]
); );
} }
...@@ -181,12 +178,12 @@ class StockHome extends StatefulComponent { ...@@ -181,12 +178,12 @@ class StockHome extends StatefulComponent {
return stocks.where((stock) => stock.symbol.contains(regexp)); return stocks.where((stock) => stock.symbol.contains(regexp));
} }
Widget buildMarketStockList() { Widget buildMarketStockList(BuildContext context) {
return new Stocklist(stocks: _filterBySearchQuery(stocks).toList()); return new Stocklist(stocks: _filterBySearchQuery(config.stocks).toList());
} }
Widget buildPortfolioStocklist() { Widget buildPortfolioStocklist(BuildContext context) {
return new Stocklist(stocks: _filterBySearchQuery(_filterByPortfolio(stocks)).toList()); return new Stocklist(stocks: _filterBySearchQuery(_filterByPortfolio(config.stocks)).toList());
} }
Widget buildTabNavigator() { Widget buildTabNavigator() {
...@@ -216,7 +213,7 @@ class StockHome extends StatefulComponent { ...@@ -216,7 +213,7 @@ class StockHome extends StatefulComponent {
return new ToolBar( return new ToolBar(
left: new IconButton( left: new IconButton(
icon: "navigation/arrow_back", icon: "navigation/arrow_back",
color: Theme.of(this).accentColor, color: Theme.of(context).accentColor,
onPressed: _handleSearchEnd onPressed: _handleSearchEnd
), ),
center: new Input( center: new Input(
...@@ -224,7 +221,7 @@ class StockHome extends StatefulComponent { ...@@ -224,7 +221,7 @@ class StockHome extends StatefulComponent {
placeholder: 'Search stocks', placeholder: 'Search stocks',
onChanged: _handleSearchQueryChanged onChanged: _handleSearchQueryChanged
), ),
backgroundColor: Theme.of(this).canvasColor backgroundColor: Theme.of(context).canvasColor
); );
} }
...@@ -255,17 +252,14 @@ class StockHome extends StatefulComponent { ...@@ -255,17 +252,14 @@ class StockHome extends StatefulComponent {
} }
Widget buildFloatingActionButton() { Widget buildFloatingActionButton() {
return new TransitionProxy( return new FloatingActionButton(
transitionKey: snackBarKey, child: new Icon(type: 'content/add', size: 24),
child: new FloatingActionButton( backgroundColor: Colors.redAccent[200],
child: new Icon(type: 'content/add', size: 24), onPressed: _handleStockPurchased
backgroundColor: Colors.redAccent[200],
onPressed: _handleStockPurchased
)
); );
} }
Widget build() { Widget build(BuildContext context) {
return new Scaffold( return new Scaffold(
toolbar: _isSearching ? buildSearchBar() : buildToolBar(), toolbar: _isSearching ? buildSearchBar() : buildToolBar(),
body: buildTabNavigator(), body: buildTabNavigator(),
......
...@@ -4,18 +4,18 @@ ...@@ -4,18 +4,18 @@
part of stocks; part of stocks;
class Stocklist extends Component { class Stocklist extends StatelessComponent {
Stocklist({ Key key, this.stocks }) : super(key: key); Stocklist({ Key key, this.stocks }) : super(key: key);
final List<Stock> stocks; final List<Stock> stocks;
Widget build() { Widget build(BuildContext context) {
return new Material( return new Material(
type: MaterialType.canvas, type: MaterialType.canvas,
child: new ScrollableList<Stock>( child: new ScrollableList<Stock>(
items: stocks, items: stocks,
itemExtent: StockRow.kHeight, itemExtent: StockRow.kHeight,
itemBuilder: (Stock stock) => new StockRow(stock: stock) itemBuilder: (BuildContext context, Stock stock) => new StockRow(stock: stock)
) )
); );
} }
......
...@@ -4,19 +4,27 @@ ...@@ -4,19 +4,27 @@
part of stocks; part of stocks;
Future showStockMenu(Navigator navigator, { bool autorefresh, ValueChanged onAutorefreshChanged }) { enum _MenuItems { add, remove, autorefresh }
return showMenu(
Future showStockMenu(NavigatorState navigator, { bool autorefresh, ValueChanged onAutorefreshChanged }) async {
switch (await showMenu(
navigator: navigator, navigator: navigator,
position: new MenuPosition( position: new MenuPosition(
right: sky.view.paddingRight, right: sky.view.paddingRight,
top: sky.view.paddingTop top: sky.view.paddingTop
), ),
builder: (Navigator navigator) { builder: (NavigatorState navigator) {
return <PopupMenuItem>[ return <PopupMenuItem>[
new PopupMenuItem(child: new Text('Add stock')),
new PopupMenuItem(child: new Text('Remove stock')),
new PopupMenuItem( new PopupMenuItem(
onPressed: () => onAutorefreshChanged(!autorefresh), value: _MenuItems.add,
child: new Text('Add stock')
),
new PopupMenuItem(
value: _MenuItems.remove,
child: new Text('Remove stock')
),
new PopupMenuItem(
value: _MenuItems.autorefresh,
child: new Row([ child: new Row([
new Flexible(child: new Text('Autorefresh')), new Flexible(child: new Text('Autorefresh')),
new Checkbox( new Checkbox(
...@@ -28,5 +36,28 @@ Future showStockMenu(Navigator navigator, { bool autorefresh, ValueChanged onAut ...@@ -28,5 +36,28 @@ Future showStockMenu(Navigator navigator, { bool autorefresh, ValueChanged onAut
), ),
]; ];
} }
); )) {
case _MenuItems.autorefresh:
onAutorefreshChanged(!autorefresh);
break;
case _MenuItems.add:
case _MenuItems.remove:
await showDialog(navigator, (NavigatorState navigator) {
return new Dialog(
title: new Text('Not Implemented'),
content: new Text('This feature has not yet been implemented.'),
actions: [
new FlatButton(
child: new Text('OH WELL'),
onPressed: () {
navigator.pop(false);
}
),
]
);
});
break;
default:
// menu was canceled.
}
} }
\ No newline at end of file
...@@ -4,15 +4,14 @@ ...@@ -4,15 +4,14 @@
part of stocks; part of stocks;
class StockRow extends Component { class StockRow extends StatelessComponent {
StockRow({ Stock stock }) : this.stock = stock, super(key: new Key(stock.symbol)); StockRow({ Stock stock }) : this.stock = stock, super(key: new Key(stock.symbol));
final Stock stock; final Stock stock;
static const double kHeight = 79.0; static const double kHeight = 79.0;
Widget build() { Widget build(BuildContext context) {
String lastSale = "\$${stock.lastSale.toStringAsFixed(2)}"; String lastSale = "\$${stock.lastSale.toStringAsFixed(2)}";
String changeInPrice = "${stock.percentChange.toStringAsFixed(2)}%"; String changeInPrice = "${stock.percentChange.toStringAsFixed(2)}%";
...@@ -32,7 +31,7 @@ class StockRow extends Component { ...@@ -32,7 +31,7 @@ class StockRow extends Component {
new Flexible( new Flexible(
child: new Text( child: new Text(
changeInPrice, changeInPrice,
style: Theme.of(this).text.caption.copyWith(textAlign: TextAlign.right) style: Theme.of(context).text.caption.copyWith(textAlign: TextAlign.right)
) )
) )
]; ];
...@@ -43,7 +42,7 @@ class StockRow extends Component { ...@@ -43,7 +42,7 @@ class StockRow extends Component {
height: kHeight, height: kHeight,
decoration: new BoxDecoration( decoration: new BoxDecoration(
border: new Border( border: new Border(
bottom: new BorderSide(color: Theme.of(this).dividerColor) bottom: new BorderSide(color: Theme.of(context).dividerColor)
) )
), ),
child: new Row([ child: new Row([
...@@ -55,7 +54,7 @@ class StockRow extends Component { ...@@ -55,7 +54,7 @@ class StockRow extends Component {
child: new Row( child: new Row(
children, children,
alignItems: FlexAlignItems.baseline, alignItems: FlexAlignItems.baseline,
textBaseline: DefaultTextStyle.of(this).textBaseline textBaseline: DefaultTextStyle.of(context).textBaseline
) )
) )
]) ])
......
...@@ -10,42 +10,32 @@ typedef void SettingsUpdater({ ...@@ -10,42 +10,32 @@ typedef void SettingsUpdater({
}); });
class StockSettings extends StatefulComponent { class StockSettings extends StatefulComponent {
const StockSettings(this.navigator, this.optimism, this.backup, this.updater);
StockSettings(this.navigator, this.optimism, this.backup, this.updater); final NavigatorState navigator;
final StockMode optimism;
final BackupMode backup;
final SettingsUpdater updater;
Navigator navigator; StockSettingsState createState() => new StockSettingsState();
StockMode optimism; }
BackupMode backup;
SettingsUpdater updater;
void syncConstructorArguments(StockSettings source) {
navigator = source.navigator;
optimism = source.optimism;
backup = source.backup;
updater = source.updater;
}
class StockSettingsState extends State<StockSettings> {
void _handleOptimismChanged(bool value) { void _handleOptimismChanged(bool value) {
setState(() { sendUpdates(value ? StockMode.optimistic : StockMode.pessimistic, config.backup);
optimism = value ? StockMode.optimistic : StockMode.pessimistic;
});
sendUpdates();
} }
void _handleBackupChanged(bool value) { void _handleBackupChanged(bool value) {
setState(() { sendUpdates(config.optimism, value ? BackupMode.enabled : BackupMode.disabled);
backup = value ? BackupMode.enabled : BackupMode.disabled;
});
sendUpdates();
} }
void _confirmOptimismChange() { void _confirmOptimismChange() {
switch (optimism) { switch (config.optimism) {
case StockMode.optimistic: case StockMode.optimistic:
_handleOptimismChanged(false); _handleOptimismChanged(false);
break; break;
case StockMode.pessimistic: case StockMode.pessimistic:
showDialog(navigator, (navigator) { showDialog(config.navigator, (NavigatorState navigator) {
return new Dialog( return new Dialog(
title: new Text("Change mode?"), title: new Text("Change mode?"),
content: new Text("Optimistic mode means everything is awesome. Are you sure you can handle that?"), content: new Text("Optimistic mode means everything is awesome. Are you sure you can handle that?"),
...@@ -72,24 +62,25 @@ class StockSettings extends StatefulComponent { ...@@ -72,24 +62,25 @@ class StockSettings extends StatefulComponent {
} }
} }
void sendUpdates() { void sendUpdates(StockMode optimism, BackupMode backup) {
if (updater != null) if (config.updater != null)
updater( config.updater(
optimism: optimism, optimism: optimism,
backup: backup backup: backup
); );
} }
Widget buildToolBar() { Widget buildToolBar(BuildContext context) {
return new ToolBar( return new ToolBar(
left: new IconButton( left: new IconButton(
icon: 'navigation/arrow_back', icon: 'navigation/arrow_back',
onPressed: navigator.pop), onPressed: config.navigator.pop
),
center: new Text('Settings') center: new Text('Settings')
); );
} }
Widget buildSettingsPane() { Widget buildSettingsPane(BuildContext context) {
// TODO(ianh): Once we have the gesture API hooked up, fix https://github.com/domokit/mojo/issues/281 // TODO(ianh): Once we have the gesture API hooked up, fix https://github.com/domokit/mojo/issues/281
// (whereby tapping the widgets below causes both the widget and the menu item to fire their callbacks) // (whereby tapping the widgets below causes both the widget and the menu item to fire their callbacks)
return new Material( return new Material(
...@@ -103,15 +94,21 @@ class StockSettings extends StatefulComponent { ...@@ -103,15 +94,21 @@ class StockSettings extends StatefulComponent {
onPressed: () => _confirmOptimismChange(), onPressed: () => _confirmOptimismChange(),
child: new Row([ child: new Row([
new Flexible(child: new Text('Everything is awesome')), new Flexible(child: new Text('Everything is awesome')),
new Checkbox(value: optimism == StockMode.optimistic, onChanged: (_) => _confirmOptimismChange()), new Checkbox(
value: config.optimism == StockMode.optimistic,
onChanged: (_) => _confirmOptimismChange()
),
]) ])
), ),
new DrawerItem( new DrawerItem(
icon: 'action/backup', icon: 'action/backup',
onPressed: () { _handleBackupChanged(!(backup == BackupMode.enabled)); }, onPressed: () { _handleBackupChanged(!(config.backup == BackupMode.enabled)); },
child: new Row([ child: new Row([
new Flexible(child: new Text('Back up stock list to the cloud')), new Flexible(child: new Text('Back up stock list to the cloud')),
new Switch(value: backup == BackupMode.enabled, onChanged: _handleBackupChanged), new Switch(
value: config.backup == BackupMode.enabled,
onChanged: _handleBackupChanged
),
]) ])
), ),
]) ])
...@@ -120,10 +117,10 @@ class StockSettings extends StatefulComponent { ...@@ -120,10 +117,10 @@ class StockSettings extends StatefulComponent {
); );
} }
Widget build() { Widget build(BuildContext context) {
return new Scaffold( return new Scaffold(
toolbar: buildToolBar(), toolbar: buildToolBar(context),
body: buildSettingsPane() body: buildSettingsPane(context)
); );
} }
} }
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
library fn3; library fn3;
export 'fn3/animated_component.dart'; export 'fn3/animated_component.dart';
export 'fn3/app.dart';
export 'fn3/basic.dart'; export 'fn3/basic.dart';
export 'fn3/binding.dart'; export 'fn3/binding.dart';
export 'fn3/button_state.dart'; export 'fn3/button_state.dart';
......
// 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:sky' as sky;
import 'package:sky/material.dart';
import 'package:sky/painting.dart';
import 'package:sky/src/fn3/basic.dart';
import 'package:sky/src/fn3/binding.dart';
import 'package:sky/src/fn3/framework.dart';
import 'package:sky/src/fn3/navigator.dart';
import 'package:sky/src/fn3/theme.dart';
import 'package:sky/src/fn3/title.dart';
const TextStyle _errorTextStyle = const TextStyle(
color: const Color(0xD0FF0000),
fontFamily: 'monospace',
fontSize: 48.0,
fontWeight: FontWeight.w900,
textAlign: TextAlign.right,
decoration: underline,
decorationColor: const Color(0xFFFF00),
decorationStyle: TextDecorationStyle.double
);
class App extends StatefulComponent {
App({
Key key,
this.title,
this.theme,
this.routes
}): super(key: key);
final String title;
final ThemeData theme;
final Map<String, RouteBuilder> routes;
AppState createState() => new AppState();
}
class AppState extends State<App> {
GlobalObjectKey _navigator;
void initState(BuildContext context) {
super.initState(context);
_navigator = new GlobalObjectKey(this);
WidgetFlutterBinding.instance.addEventListener(_backHandler);
}
void dispose() {
WidgetFlutterBinding.instance.removeEventListener(_backHandler);
super.dispose();
}
void _backHandler(sky.Event event) {
assert(mounted);
if (event.type == 'back') {
NavigatorState navigator = _navigator.currentState;
assert(navigator != null);
if (navigator.hasPreviousRoute)
navigator.pop();
}
}
Widget build(BuildContext context) {
return new Theme(
data: config.theme,
child: new DefaultTextStyle(
style: _errorTextStyle,
child: new Title(
title: config.title,
child: new Navigator(
key: _navigator,
routes: config.routes
)
)
)
);
}
}
\ No newline at end of file
...@@ -44,10 +44,11 @@ class Checkbox extends StatelessComponent { ...@@ -44,10 +44,11 @@ class Checkbox extends StatelessComponent {
? _kLightUncheckedColor ? _kLightUncheckedColor
: _kDarkUncheckedColor; : _kDarkUncheckedColor;
return new _CheckboxWrapper( return new _CheckboxWrapper(
value: value, value: value,
onChanged: onChanged, onChanged: onChanged,
uncheckedColor: uncheckedColor, uncheckedColor: uncheckedColor,
accentColor: themeData.accentColor); accentColor: themeData.accentColor
);
} }
} }
...@@ -55,9 +56,16 @@ class Checkbox extends StatelessComponent { ...@@ -55,9 +56,16 @@ class Checkbox extends StatelessComponent {
// order to get an accent color from a Theme but Components do not know how to // order to get an accent color from a Theme but Components do not know how to
// host RenderObjects. // host RenderObjects.
class _CheckboxWrapper extends LeafRenderObjectWidget { class _CheckboxWrapper extends LeafRenderObjectWidget {
_CheckboxWrapper({Key key, this.value, this.onChanged, this.uncheckedColor, _CheckboxWrapper({
this.accentColor}) Key key,
: super(key: key); this.value,
this.onChanged,
this.uncheckedColor,
this.accentColor
}): super(key: key) {
assert(uncheckedColor != null);
assert(accentColor != null);
}
final bool value; final bool value;
final ValueChanged onChanged; final ValueChanged onChanged;
...@@ -65,7 +73,11 @@ class _CheckboxWrapper extends LeafRenderObjectWidget { ...@@ -65,7 +73,11 @@ class _CheckboxWrapper extends LeafRenderObjectWidget {
final Color accentColor; final Color accentColor;
_RenderCheckbox createRenderObject() => new _RenderCheckbox( _RenderCheckbox createRenderObject() => new _RenderCheckbox(
value: value, uncheckedColor: uncheckedColor, onChanged: onChanged); value: value,
accentColor: accentColor,
uncheckedColor: uncheckedColor,
onChanged: onChanged
);
void updateRenderObject(_RenderCheckbox renderObject, _CheckboxWrapper oldWidget) { void updateRenderObject(_RenderCheckbox renderObject, _CheckboxWrapper oldWidget) {
renderObject.value = value; renderObject.value = value;
...@@ -76,25 +88,38 @@ class _CheckboxWrapper extends LeafRenderObjectWidget { ...@@ -76,25 +88,38 @@ class _CheckboxWrapper extends LeafRenderObjectWidget {
} }
class _RenderCheckbox extends RenderToggleable { class _RenderCheckbox extends RenderToggleable {
_RenderCheckbox({bool value, Color uncheckedColor, ValueChanged onChanged}) _RenderCheckbox({
: _uncheckedColor = uncheckedColor, bool value,
super( Color uncheckedColor,
value: value, Color accentColor,
onChanged: onChanged, ValueChanged onChanged
size: new Size(_kEdgeSize, _kEdgeSize)) {} }): _uncheckedColor = uncheckedColor,
_accentColor = accentColor,
super(
value: value,
onChanged: onChanged,
size: new Size(_kEdgeSize, _kEdgeSize)
) {
assert(uncheckedColor != null);
assert(accentColor != null);
}
Color _uncheckedColor; Color _uncheckedColor;
Color get uncheckedColor => _uncheckedColor; Color get uncheckedColor => _uncheckedColor;
void set uncheckedColor(Color value) { void set uncheckedColor(Color value) {
if (value == _uncheckedColor) return; assert(value != null);
if (value == _uncheckedColor)
return;
_uncheckedColor = value; _uncheckedColor = value;
markNeedsPaint(); markNeedsPaint();
} }
Color _accentColor; Color _accentColor;
void set accentColor(Color value) { void set accentColor(Color value) {
if (value == _accentColor) return; assert(value != null);
if (value == _accentColor)
return;
_accentColor = value; _accentColor = value;
markNeedsPaint(); markNeedsPaint();
} }
......
...@@ -16,7 +16,7 @@ import 'package:sky/src/fn3/scrollable.dart'; ...@@ -16,7 +16,7 @@ import 'package:sky/src/fn3/scrollable.dart';
import 'package:sky/src/fn3/theme.dart'; import 'package:sky/src/fn3/theme.dart';
import 'package:sky/src/fn3/transitions.dart'; import 'package:sky/src/fn3/transitions.dart';
typedef Widget DialogBuilder(Navigator navigator); typedef Dialog DialogBuilder(NavigatorState navigator);
/// A material design dialog /// A material design dialog
/// ///
...@@ -132,7 +132,7 @@ class Dialog extends StatelessComponent { ...@@ -132,7 +132,7 @@ class Dialog extends StatelessComponent {
const Duration _kTransitionDuration = const Duration(milliseconds: 150); const Duration _kTransitionDuration = const Duration(milliseconds: 150);
class DialogRoute extends RouteBase { class DialogRoute extends Route {
DialogRoute({ this.completer, this.builder }); DialogRoute({ this.completer, this.builder });
final Completer completer; final Completer completer;
......
...@@ -8,11 +8,11 @@ import 'package:sky/src/fn3/focus.dart'; ...@@ -8,11 +8,11 @@ import 'package:sky/src/fn3/focus.dart';
import 'package:sky/src/fn3/framework.dart'; import 'package:sky/src/fn3/framework.dart';
import 'package:sky/src/fn3/transitions.dart'; import 'package:sky/src/fn3/transitions.dart';
typedef Widget RouteBuilder(NavigatorState navigator, RouteBase route); typedef Widget RouteBuilder(NavigatorState navigator, Route route);
typedef void NotificationCallback(); typedef void NotificationCallback();
abstract class RouteBase { abstract class Route {
AnimationPerformance _performance; AnimationPerformance _performance;
NotificationCallback onDismissed; NotificationCallback onDismissed;
NotificationCallback onCompleted; NotificationCallback onCompleted;
...@@ -57,10 +57,10 @@ abstract class RouteBase { ...@@ -57,10 +57,10 @@ abstract class RouteBase {
const Duration _kTransitionDuration = const Duration(milliseconds: 150); const Duration _kTransitionDuration = const Duration(milliseconds: 150);
const Point _kTransitionStartPoint = const Point(0.0, 75.0); const Point _kTransitionStartPoint = const Point(0.0, 75.0);
class Route extends RouteBase {
Route({ this.name, this.builder });
final String name; class PageRoute extends Route {
PageRoute(this.builder);
final RouteBuilder builder; final RouteBuilder builder;
bool get isOpaque => true; bool get isOpaque => true;
...@@ -81,16 +81,16 @@ class Route extends RouteBase { ...@@ -81,16 +81,16 @@ class Route extends RouteBase {
) )
); );
} }
String toString() => '$runtimeType(name="$name")';
} }
class RouteState extends RouteBase { typedef void RouteStateCallback(RouteState route);
RouteState({ this.callback, this.route, this.owner });
class RouteState extends Route {
RouteState({ this.route, this.owner, this.callback });
Function callback; Route route;
RouteBase route;
State owner; State owner;
RouteStateCallback callback;
bool get isOpaque => false; bool get isOpaque => false;
...@@ -105,99 +105,81 @@ class RouteState extends RouteBase { ...@@ -105,99 +105,81 @@ class RouteState extends RouteBase {
Widget build(Key key, NavigatorState navigator, WatchableAnimationPerformance performance) => null; Widget build(Key key, NavigatorState navigator, WatchableAnimationPerformance performance) => null;
} }
class NavigatorHistory { class Navigator extends StatefulComponent {
Navigator({ this.routes, Key key }) : super(key: key) {
NavigatorHistory(List<Route> routes) { // To use a navigator, you must at a minimum define the route with the name '/'.
for (Route route in routes) { assert(routes.containsKey('/'));
if (route.name != null)
namedRoutes[route.name] = route;
}
recents.add(routes[0]);
}
List<RouteBase> recents = new List<RouteBase>();
int index = 0;
Map<String, RouteBase> namedRoutes = new Map<String, RouteBase>();
RouteBase get currentRoute => recents[index];
bool hasPrevious() => index > 0;
void pushNamed(String name) {
Route route = namedRoutes[name];
assert(route != null);
push(route);
}
void push(RouteBase route) {
assert(!_debugCurrentlyHaveRoute(route));
recents.insert(index + 1, route);
index++;
} }
void pop([dynamic result]) { final Map<String, RouteBuilder> routes;
if (index > 0) {
RouteBase route = recents[index];
route.popState(result);
index--;
}
}
bool _debugCurrentlyHaveRoute(RouteBase route) { NavigatorState createState() => new NavigatorState();
return recents.any((candidate) => candidate == route);
}
} }
class Navigator extends StatefulComponent { class NavigatorState extends State<Navigator> {
Navigator(this.history, { Key key }) : super(key: key);
final NavigatorHistory history; List<Route> _history = new List<Route>();
int _currentPosition = 0;
NavigatorState createState() => new NavigatorState(); Route get currentRoute => _history[_currentPosition];
} bool get hasPreviousRoute => _history.length > 1;
class NavigatorState extends State<Navigator> { void initState(BuildContext context) {
RouteBase get currentRoute => config.history.currentRoute; super.initState(context);
PageRoute route = new PageRoute(config.routes['/']);
assert(route != null);
_history.add(route);
}
void pushState(State owner, Function callback) { void pushState(State owner, Function callback) {
RouteBase route = new RouteState( push(new RouteState(
route: currentRoute,
owner: owner, owner: owner,
callback: callback, callback: callback
route: currentRoute ));
);
push(route);
} }
void pushNamed(String name) { void pushNamed(String name) {
setState(() { PageRoute route = new PageRoute(config.routes[name]);
config.history.pushNamed(name); assert(route != null);
}); push(route);
} }
void push(RouteBase route) { void push(Route route) {
assert(!_debugCurrentlyHaveRoute(route));
_history.insert(_currentPosition + 1, route);
setState(() { setState(() {
config.history.push(route); _currentPosition += 1;
}); });
} }
void pop([dynamic result]) { void pop([dynamic result]) {
setState(() { if (_currentPosition > 0) {
config.history.pop(result); Route route = _history[_currentPosition];
}); route.popState(result);
setState(() {
_currentPosition -= 1;
});
}
}
bool _debugCurrentlyHaveRoute(Route route) {
return _history.any((candidate) => candidate == route);
} }
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<Widget> visibleRoutes = new List<Widget>(); List<Widget> visibleRoutes = new List<Widget>();
for (int i = config.history.recents.length-1; i >= 0; i -= 1) { for (int i = _history.length-1; i >= 0; i -= 1) {
RouteBase route = config.history.recents[i]; Route route = _history[i];
if (!route.hasContent) if (!route.hasContent)
continue; continue;
WatchableAnimationPerformance performance = route.ensurePerformance( WatchableAnimationPerformance performance = route.ensurePerformance(
direction: (i <= config.history.index) ? Direction.forward : Direction.reverse direction: (i <= _currentPosition) ? Direction.forward : Direction.reverse
); );
route.onDismissed = () { route.onDismissed = () {
setState(() { setState(() {
assert(config.history.recents.contains(route)); assert(_history.contains(route));
config.history.recents.remove(route); _history.remove(route);
}); });
}; };
Key key = new ObjectKey(route); Key key = new ObjectKey(route);
......
...@@ -6,8 +6,8 @@ import 'dart:async'; ...@@ -6,8 +6,8 @@ import 'dart:async';
import 'dart:sky' as sky; import 'dart:sky' as sky;
import 'package:sky/animation.dart'; import 'package:sky/animation.dart';
import 'package:sky/painting.dart';
import 'package:sky/material.dart'; import 'package:sky/material.dart';
import 'package:sky/painting.dart';
import 'package:sky/src/fn3/basic.dart'; import 'package:sky/src/fn3/basic.dart';
import 'package:sky/src/fn3/focus.dart'; import 'package:sky/src/fn3/focus.dart';
import 'package:sky/src/fn3/framework.dart'; import 'package:sky/src/fn3/framework.dart';
...@@ -15,6 +15,7 @@ import 'package:sky/src/fn3/gesture_detector.dart'; ...@@ -15,6 +15,7 @@ import 'package:sky/src/fn3/gesture_detector.dart';
import 'package:sky/src/fn3/navigator.dart'; import 'package:sky/src/fn3/navigator.dart';
import 'package:sky/src/fn3/popup_menu_item.dart'; import 'package:sky/src/fn3/popup_menu_item.dart';
import 'package:sky/src/fn3/scrollable.dart'; import 'package:sky/src/fn3/scrollable.dart';
import 'package:sky/src/fn3/theme.dart';
import 'package:sky/src/fn3/transitions.dart'; import 'package:sky/src/fn3/transitions.dart';
const Duration _kMenuDuration = const Duration(milliseconds: 300); const Duration _kMenuDuration = const Duration(milliseconds: 300);
...@@ -26,6 +27,8 @@ const double _kMenuMaxWidth = 5.0 * _kMenuWidthStep; ...@@ -26,6 +27,8 @@ const double _kMenuMaxWidth = 5.0 * _kMenuWidthStep;
const double _kMenuHorizontalPadding = 16.0; const double _kMenuHorizontalPadding = 16.0;
const double _kMenuVerticalPadding = 8.0; const double _kMenuVerticalPadding = 8.0;
typedef List<PopupMenuItem> PopupMenuItemsBuilder(NavigatorState navigator);
class PopupMenu extends StatefulComponent { class PopupMenu extends StatefulComponent {
PopupMenu({ PopupMenu({
Key key, Key key,
...@@ -49,25 +52,10 @@ class PopupMenu extends StatefulComponent { ...@@ -49,25 +52,10 @@ class PopupMenu extends StatefulComponent {
class PopupMenuState extends State<PopupMenu> { class PopupMenuState extends State<PopupMenu> {
void initState(BuildContext context) { void initState(BuildContext context) {
super.initState(context); super.initState(context);
_updateBoxPainter();
config.performance.addListener(_performanceChanged); config.performance.addListener(_performanceChanged);
} }
BoxPainter _painter;
void _updateBoxPainter() {
_painter = new BoxPainter(
new BoxDecoration(
backgroundColor: Colors.grey[50],
borderRadius: 2.0,
boxShadow: shadows[config.level]
)
);
}
void didUpdateConfig(PopupMenu oldConfig) { void didUpdateConfig(PopupMenu oldConfig) {
if (config.level != config.level)
_updateBoxPainter();
if (config.performance != oldConfig.performance) { if (config.performance != oldConfig.performance) {
oldConfig.performance.removeListener(_performanceChanged); oldConfig.performance.removeListener(_performanceChanged);
config.performance.addListener(_performanceChanged); config.performance.addListener(_performanceChanged);
...@@ -85,7 +73,19 @@ class PopupMenuState extends State<PopupMenu> { ...@@ -85,7 +73,19 @@ class PopupMenuState extends State<PopupMenu> {
}); });
} }
BoxPainter _painter;
void _updateBoxPainter(BoxDecoration decoration) {
if (_painter == null || _painter.decoration != decoration)
_painter = new BoxPainter(decoration);
}
Widget build(BuildContext context) { Widget build(BuildContext context) {
_updateBoxPainter(new BoxDecoration(
backgroundColor: Theme.of(context).canvasColor,
borderRadius: 2.0,
boxShadow: shadows[config.level]
));
double unit = 1.0 / (config.items.length + 1.5); // 1.0 for the width and 0.5 for the last item's fade. double unit = 1.0 / (config.items.length + 1.5); // 1.0 for the width and 0.5 for the last item's fade.
List<Widget> children = []; List<Widget> children = [];
for (int i = 0; i < config.items.length; ++i) { for (int i = 0; i < config.items.length; ++i) {
...@@ -151,7 +151,7 @@ class MenuPosition { ...@@ -151,7 +151,7 @@ class MenuPosition {
final double left; final double left;
} }
class MenuRoute extends RouteBase { class MenuRoute extends Route {
MenuRoute({ this.completer, this.position, this.builder, this.level }); MenuRoute({ this.completer, this.position, this.builder, this.level });
final Completer completer; final Completer completer;
...@@ -194,8 +194,6 @@ class MenuRoute extends RouteBase { ...@@ -194,8 +194,6 @@ class MenuRoute extends RouteBase {
} }
} }
typedef List<PopupMenuItem> PopupMenuItemsBuilder(NavigatorState navigator);
Future showMenu({ NavigatorState navigator, MenuPosition position, PopupMenuItemsBuilder builder, int level: 4 }) { Future showMenu({ NavigatorState navigator, MenuPosition position, PopupMenuItemsBuilder builder, int level: 4 }) {
Completer completer = new Completer(); Completer completer = new Completer();
navigator.push(new MenuRoute( navigator.push(new MenuRoute(
......
...@@ -63,6 +63,7 @@ class Typography { ...@@ -63,6 +63,7 @@ class Typography {
// TODO(abarth): Maybe this should be hard-coded in Scaffold? // TODO(abarth): Maybe this should be hard-coded in Scaffold?
static const String typeface = 'font-family: sans-serif'; static const String typeface = 'font-family: sans-serif';
// TODO(ianh): Remove this when we remove fn2, now that it's hard-coded in App.
static const TextStyle error = const TextStyle( static const TextStyle error = const TextStyle(
color: const Color(0xD0FF0000), color: const Color(0xD0FF0000),
fontFamily: 'monospace', fontFamily: 'monospace',
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
// TODO(ianh): rename this file 'binding.dart'
import 'dart:sky' as sky; import 'dart:sky' as sky;
import 'package:sky/animation.dart'; import 'package:sky/animation.dart';
...@@ -39,6 +41,7 @@ class BindingHitTestEntry extends HitTestEntry { ...@@ -39,6 +41,7 @@ class BindingHitTestEntry extends HitTestEntry {
} }
/// The glue between the render tree and the sky engine /// The glue between the render tree and the sky engine
// TODO(ianh): rename this class FlutterBinding
class SkyBinding extends HitTestTarget { class SkyBinding extends HitTestTarget {
SkyBinding({ RenderBox root: null, RenderView renderViewOverride }) { SkyBinding({ RenderBox root: null, RenderView renderViewOverride }) {
......
...@@ -50,18 +50,12 @@ void main() { ...@@ -50,18 +50,12 @@ void main() {
test('Can navigator navigate to and from a stateful component', () { test('Can navigator navigate to and from a stateful component', () {
WidgetTester tester = new WidgetTester(); WidgetTester tester = new WidgetTester();
final NavigatorHistory routes = new NavigatorHistory([ final Map<String, RouteBuilder> routes = <String, RouteBuilder>{
new Route( '/': (navigator, route) => new FirstComponent(navigator),
name: '/', '/second': (navigator, route) => new SecondComponent(navigator),
builder: (navigator, route) => new FirstComponent(navigator) };
),
new Route(
name: '/second',
builder: (navigator, route) => new SecondComponent(navigator)
)
]);
tester.pumpFrame(new Navigator(routes)); tester.pumpFrame(new Navigator(routes: routes));
expect(tester.findText('X'), isNotNull); expect(tester.findText('X'), isNotNull);
expect(tester.findText('Y'), isNull); expect(tester.findText('Y'), isNull);
......
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