blob: 754a54929e6739c0fb7c1a777a4eb7a58c1ba239 [file] [edit]
// Copyright 2023 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 'package:devtools_app_shared/ui.dart';
import 'package:flutter/material.dart';
import 'package:pointer_interceptor/pointer_interceptor.dart';
import '../shared/common_widgets.dart';
import '../shared/primitives/utils.dart';
import '../shared/screen.dart';
class DevToolsAppBar extends StatelessWidget {
const DevToolsAppBar({
super.key,
required this.tabController,
required this.screens,
required this.actions,
});
final TabController? tabController;
final List<Screen> screens;
final List<Widget>? actions;
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
Widget? flexibleSpace;
TabBar tabBar;
List<Screen> visibleScreens = screens;
bool tabsOverflow({bool includeOverflowButtonWidth = false}) {
return _scaffoldHeaderWidth(
screens: visibleScreens,
actions: actions,
textTheme: textTheme,
) +
(includeOverflowButtonWidth ? TabOverflowButton.width : 0) >=
MediaQuery.of(context).size.width;
}
var overflow = tabsOverflow();
while (overflow) {
visibleScreens = List.of(visibleScreens)..safeRemoveLast();
overflow = tabsOverflow(includeOverflowButtonWidth: true);
if (overflow && visibleScreens.isEmpty) {
break;
}
}
final overflowScreens = screens.sublist(visibleScreens.length);
// Add a leading [VerticalLineSpacer] to the actions to separate them from
// the tabs.
final actionsWithSpacer = List<Widget>.from(actions ?? [])
..insert(0, VerticalLineSpacer(height: defaultToolbarHeight));
final hasMultipleTabs = screens.length > 1;
if (hasMultipleTabs) {
tabBar = TabBar(
controller: tabController,
isScrollable: true,
labelPadding: EdgeInsets.zero,
tabs: [
for (final screen in visibleScreens)
Padding(
padding: const EdgeInsets.symmetric(horizontal: tabBarSpacing),
child: screen.buildTab(context),
),
// We need to include a widget in the tab bar for the overflow screens
// because the [_tabController] expects a length equal to the total
// number of screens, hidden or not.
for (final _ in overflowScreens) const SizedBox.shrink(),
],
);
final rightPadding = math.max(
0.0,
// Use [actions] here instead of [actionsWithSpacer] because we may
// have added a spacer element to [actionsWithSpacer] above, which
// should be excluded from the width calculation.
actionWidgetSize * ((actions ?? []).length) +
VerticalLineSpacer.totalWidth,
);
flexibleSpace = Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.only(
top: densePadding,
right: rightPadding,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
tabBar,
if (overflowScreens.isNotEmpty)
Expanded(
child: Align(
alignment: Alignment.centerLeft,
child: TabOverflowButton(
screens: overflowScreens,
selectedIndex:
tabController!.index - visibleScreens.length,
onItemSelected: (index) {
final selectedTabIndex = visibleScreens.length + index;
tabController!.index = selectedTabIndex;
},
),
),
),
],
),
),
);
}
return AppBar(
// Turn off the appbar's back button.
automaticallyImplyLeading: false,
centerTitle: false,
toolbarHeight: defaultToolbarHeight,
actions: actionsWithSpacer,
flexibleSpace: flexibleSpace,
);
}
/// Returns the width of the scaffold title, tabs and default icons.
double _scaffoldHeaderWidth({
required List<Screen> screens,
required List<Widget>? actions,
required TextTheme textTheme,
}) {
final tabsWidth = screens.fold(
0.0,
(prev, screen) => prev + screen.approximateTabWidth(textTheme),
);
final actionsWidth = (actions?.length ?? 0) * actionWidgetSize;
return tabsWidth + VerticalLineSpacer.totalWidth + actionsWidth;
}
}
class TabOverflowButton extends StatelessWidget {
const TabOverflowButton({
super.key,
required this.screens,
required this.selectedIndex,
required this.onItemSelected,
});
static const width = 40.0;
final List<Screen> screens;
final int selectedIndex;
bool get overflowTabSelected => selectedIndex >= 0;
final void Function(int) onItemSelected;
@override
Widget build(BuildContext context) {
final selectedColor = Theme.of(context).colorScheme.primary;
final button = ContextMenuButton(
icon: Icons.keyboard_double_arrow_right_sharp,
iconSize: actionsIconSize,
color: overflowTabSelected ? selectedColor : null,
buttonWidth: buttonMinWidth,
menuChildren: _buildChildren(context),
);
return overflowTabSelected ? SelectedTabWrapper(child: button) : button;
}
List<Widget> _buildChildren(BuildContext context) {
final theme = Theme.of(context);
final children = <Widget>[];
for (int i = 0; i < screens.length; i++) {
final screen = screens[i];
var tab = screen.buildTab(context);
if (i == selectedIndex) {
final tabWidth = screen.approximateTabWidth(
theme.textTheme,
includeTabBarSpacing: false,
);
tab = SelectedTabWrapper(
width: tabWidth,
child: Container(
width: tabWidth,
alignment: Alignment.center,
child: tab,
),
);
}
children.add(
PointerInterceptor(
child: SizedBox(
// Match the height of the main tab bar.
height: defaultToolbarHeight,
child: MenuItemButton(
style: const ButtonStyle().copyWith(
textStyle: WidgetStateProperty.resolveWith<TextStyle>((_) {
return theme.textTheme.titleMedium!;
}),
),
onPressed: () => onItemSelected(i),
child: tab,
),
),
),
);
}
return children;
}
}
@visibleForTesting
class SelectedTabWrapper extends StatelessWidget {
SelectedTabWrapper({super.key, required this.child, double? width})
: width = width ?? buttonMinWidth;
final Widget child;
final double width;
static const _selectedIndicatorHeight = 3.0;
@override
Widget build(BuildContext context) {
final selectedColor = Theme.of(context).colorScheme.primary;
const radius = Radius.circular(_selectedIndicatorHeight);
return Stack(
children: [
child,
Positioned(
bottom: 0.0,
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: radius,
topRight: radius,
),
child: Container(
height: _selectedIndicatorHeight,
width: width,
color: selectedColor,
),
),
),
],
);
}
}