1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'package:flutter/cupertino.dart';
6/// @docImport 'package:flutter/material.dart';
7library;
8
9import 'dart:math' as math;
10
11import 'basic.dart';
12import 'debug.dart';
13import 'framework.dart';
14
15/// [NavigationToolbar] is a layout helper to position 3 widgets or groups of
16/// widgets along a horizontal axis that's sensible for an application's
17/// navigation bar such as in Material Design and in iOS.
18///
19/// The [leading] and [trailing] widgets occupy the edges of the widget with
20/// reasonable size constraints while the [middle] widget occupies the remaining
21/// space in either a center aligned or start aligned fashion.
22///
23/// Either directly use the themed app bars such as the Material [AppBar] or
24/// the iOS [CupertinoNavigationBar] or wrap this widget with more theming
25/// specifications for your own custom app bar.
26class NavigationToolbar extends StatelessWidget {
27 /// Creates a widget that lays out its children in a manner suitable for a
28 /// toolbar.
29 const NavigationToolbar({
30 super.key,
31 this.leading,
32 this.middle,
33 this.trailing,
34 this.centerMiddle = true,
35 this.middleSpacing = kMiddleSpacing,
36 });
37
38 /// The default spacing around the [middle] widget in dp.
39 static const double kMiddleSpacing = 16.0;
40
41 /// Widget to place at the start of the horizontal toolbar.
42 final Widget? leading;
43
44 /// Widget to place in the middle of the horizontal toolbar, occupying
45 /// as much remaining space as possible.
46 final Widget? middle;
47
48 /// Widget to place at the end of the horizontal toolbar.
49 final Widget? trailing;
50
51 /// Whether to align the [middle] widget to the center of this widget or
52 /// next to the [leading] widget when false.
53 final bool centerMiddle;
54
55 /// The spacing around the [middle] widget on horizontal axis.
56 ///
57 /// Defaults to [kMiddleSpacing].
58 final double middleSpacing;
59
60 @override
61 Widget build(BuildContext context) {
62 assert(debugCheckHasDirectionality(context));
63 final TextDirection textDirection = Directionality.of(context);
64 return CustomMultiChildLayout(
65 delegate: _ToolbarLayout(
66 centerMiddle: centerMiddle,
67 middleSpacing: middleSpacing,
68 textDirection: textDirection,
69 ),
70 children: <Widget>[
71 if (leading != null) LayoutId(id: _ToolbarSlot.leading, child: leading!),
72 if (middle != null) LayoutId(id: _ToolbarSlot.middle, child: middle!),
73 if (trailing != null) LayoutId(id: _ToolbarSlot.trailing, child: trailing!),
74 ],
75 );
76 }
77}
78
79enum _ToolbarSlot { leading, middle, trailing }
80
81class _ToolbarLayout extends MultiChildLayoutDelegate {
82 _ToolbarLayout({
83 required this.centerMiddle,
84 required this.middleSpacing,
85 required this.textDirection,
86 });
87
88 // If false the middle widget should be start-justified within the space
89 // between the leading and trailing widgets.
90 // If true the middle widget is centered within the toolbar (not within the horizontal
91 // space between the leading and trailing widgets).
92 final bool centerMiddle;
93
94 /// The spacing around middle widget on horizontal axis.
95 final double middleSpacing;
96
97 final TextDirection textDirection;
98
99 @override
100 void performLayout(Size size) {
101 double leadingWidth = 0.0;
102 double trailingWidth = 0.0;
103
104 if (hasChild(_ToolbarSlot.leading)) {
105 final BoxConstraints constraints = BoxConstraints(
106 maxWidth: size.width,
107 minHeight: size.height, // The height should be exactly the height of the bar.
108 maxHeight: size.height,
109 );
110 leadingWidth = layoutChild(_ToolbarSlot.leading, constraints).width;
111 final double leadingX = switch (textDirection) {
112 TextDirection.rtl => size.width - leadingWidth,
113 TextDirection.ltr => 0.0,
114 };
115 positionChild(_ToolbarSlot.leading, Offset(leadingX, 0.0));
116 }
117
118 if (hasChild(_ToolbarSlot.trailing)) {
119 final BoxConstraints constraints = BoxConstraints.loose(size);
120 final Size trailingSize = layoutChild(_ToolbarSlot.trailing, constraints);
121 final double trailingX = switch (textDirection) {
122 TextDirection.rtl => 0.0,
123 TextDirection.ltr => size.width - trailingSize.width,
124 };
125 final double trailingY = (size.height - trailingSize.height) / 2.0;
126 trailingWidth = trailingSize.width;
127 positionChild(_ToolbarSlot.trailing, Offset(trailingX, trailingY));
128 }
129
130 if (hasChild(_ToolbarSlot.middle)) {
131 final double maxWidth = math.max(
132 size.width - leadingWidth - trailingWidth - middleSpacing * 2.0,
133 0.0,
134 );
135 final BoxConstraints constraints = BoxConstraints.loose(size).copyWith(maxWidth: maxWidth);
136 final Size middleSize = layoutChild(_ToolbarSlot.middle, constraints);
137
138 final double middleStartMargin = leadingWidth + middleSpacing;
139 double middleStart = middleStartMargin;
140 final double middleY = (size.height - middleSize.height) / 2.0;
141 // If the centered middle will not fit between the leading and trailing
142 // widgets, then align its left or right edge with the adjacent boundary.
143 if (centerMiddle) {
144 middleStart = (size.width - middleSize.width) / 2.0;
145 if (middleStart + middleSize.width > size.width - trailingWidth) {
146 middleStart = size.width - trailingWidth - middleSize.width - middleSpacing;
147 } else if (middleStart < middleStartMargin) {
148 middleStart = middleStartMargin;
149 }
150 }
151
152 final double middleX = switch (textDirection) {
153 TextDirection.rtl => size.width - middleSize.width - middleStart,
154 TextDirection.ltr => middleStart,
155 };
156
157 positionChild(_ToolbarSlot.middle, Offset(middleX, middleY));
158 }
159 }
160
161 @override
162 bool shouldRelayout(_ToolbarLayout oldDelegate) {
163 return oldDelegate.centerMiddle != centerMiddle ||
164 oldDelegate.middleSpacing != middleSpacing ||
165 oldDelegate.textDirection != textDirection;
166 }
167}
168