blob: 2da8197965eb7bd93643befa5378ab49db23a41a [file] [log] [blame]
// Copyright 2019 The Fuchsia 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 '../blocs/tabs_bloc.dart';
import '../blocs/webpage_bloc.dart';
import '../models/tabs_action.dart';
const _kTabBarHeight = 24.0;
const _kMinTabWidth = 120.0;
const _kSeparatorWidth = 1.0;
const _kTabPadding = EdgeInsets.symmetric(horizontal: _kTabBarHeight);
const _kScrollToMargin = _kMinTabWidth * 0.333;
class TabsWidget extends StatefulWidget {
final TabsBloc<WebPageBloc> bloc;
const TabsWidget({@required this.bloc});
@override
_TabsWidgetState createState() => _TabsWidgetState();
}
class _TabsWidgetState extends State<TabsWidget> {
final _scrollController = ScrollController();
@override
void initState() {
super.initState();
_setupBloc(null, widget);
}
@override
void dispose() {
_setupBloc(widget, null);
_scrollController.dispose();
super.dispose();
}
@override
void didUpdateWidget(TabsWidget oldWidget) {
super.didUpdateWidget(oldWidget);
_setupBloc(oldWidget, widget);
}
void _setupBloc(TabsWidget oldWidget, TabsWidget newWidget) {
if (oldWidget?.bloc != newWidget?.bloc) {
oldWidget?.bloc?.currentTabNotifier?.removeListener(_onCurrentTabChanged);
widget?.bloc?.currentTabNotifier?.addListener(_onCurrentTabChanged);
}
}
void _onCurrentTabChanged() {
if (_scrollController.hasClients) {
final viewportWidth = _scrollController.position.viewportDimension;
final currentTabIndex = widget.bloc.tabs.indexOf(widget.bloc.currentTab);
final currentTabPosition =
currentTabIndex * (_kMinTabWidth + _kSeparatorWidth);
final offsetForLeftEdge = currentTabPosition - _kScrollToMargin;
final offsetForRightEdge =
currentTabPosition - viewportWidth + _kMinTabWidth + _kScrollToMargin;
double newOffset;
if (_scrollController.offset > offsetForLeftEdge) {
newOffset = offsetForLeftEdge;
} else if (_scrollController.offset < offsetForRightEdge) {
newOffset = offsetForRightEdge;
}
if (newOffset != null) {
_scrollController.animateTo(
newOffset,
duration: Duration(milliseconds: 300),
curve: Curves.ease,
);
}
}
}
@override
Widget build(BuildContext context) => AnimatedBuilder(
animation: Listenable.merge(
[widget.bloc.tabsNotifier, widget.bloc.currentTabNotifier]),
builder: (_, __) => widget.bloc.tabs.length > 1
? Container(
height: _kTabBarHeight,
color: Theme.of(context).accentColor,
padding: EdgeInsets.symmetric(vertical: 1.0),
child: LayoutBuilder(
builder: (context, constraints) => widget.bloc.tabs.length <
constraints.maxWidth / _kMinTabWidth
? Row(children: _buildPageTabs(width: null))
: ListView(
controller: _scrollController,
scrollDirection: Axis.horizontal,
children: _buildPageTabs(width: _kMinTabWidth),
),
),
)
: Offstage(),
);
List<Widget> _buildPageTabs({@required double width}) => widget.bloc.tabs
.map(_buildTab)
// add a 1pip separator before every tab,
// divide the rest of the space between tabs
.expand((item) => [
SizedBox(width: _kSeparatorWidth),
width == null
? Expanded(child: item, flex: 1)
: SizedBox(child: item, width: width),
])
// skip the first separator
.skip(1)
.toList();
Widget _buildTab(WebPageBloc tab) => _TabWidget(
bloc: tab,
selected: tab == widget.bloc.currentTab,
onSelect: () => widget.bloc.request.add(FocusTabAction(tab: tab)),
onClose: () => widget.bloc.request.add(RemoveTabAction(tab: tab)),
);
}
class _TabWidget extends StatefulWidget {
const _TabWidget({this.bloc, this.selected, this.onSelect, this.onClose});
final WebPageBloc bloc;
final bool selected;
final VoidCallback onSelect;
final VoidCallback onClose;
@override
_TabWidgetState createState() => _TabWidgetState();
}
class _TabWidgetState extends State<_TabWidget> {
final _hovering = ValueNotifier<bool>(false);
@override
Widget build(BuildContext context) {
final baseTheme = Theme.of(context);
return MouseRegion(
onEnter: (_) {
_hovering.value = true;
},
onExit: (_) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_hovering.value = false;
});
},
child: GestureDetector(
onTap: widget.onSelect,
child: Container(
color:
widget.selected ? baseTheme.accentColor : baseTheme.primaryColor,
child: DefaultTextStyle(
style: baseTheme.textTheme.body1.copyWith(
color: widget.selected ? baseTheme.primaryColor : null,
),
child: Stack(
children: <Widget>[
Center(
child: Padding(
padding: _kTabPadding,
child: AnimatedBuilder(
animation: widget.bloc.pageTitleNotifier,
builder: (_, __) => Text(
widget.bloc.pageTitle ?? 'NEW TAB',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
),
Positioned(
top: 0.0,
right: 0.0,
bottom: 0.0,
child: AnimatedBuilder(
animation: _hovering,
builder: (_, child) => Offstage(
offstage: !(widget.selected || _hovering.value),
child: child,
),
child: AspectRatio(
aspectRatio: 1.0,
child: GestureDetector(
onTap: widget.onClose,
child: Container(
color: Colors.transparent,
alignment: Alignment.center,
child: Text('×'),
),
),
),
),
),
],
),
),
),
),
);
}
}