Skip to content

Commit 79ada37

Browse files
committed
app_bar [nfc]: Centralize _getEffectiveCenterTitle in our wrapper
This logic is sort of complicated, and duplicated from upstream. Better to put it centrally in our ZulipAppBar wrapper, instead of in message_list.dart. As a bonus, we also have it handle `actions` instead of assuming there are none.
1 parent dbe1cad commit 79ada37

File tree

3 files changed

+186
-33
lines changed

3 files changed

+186
-33
lines changed

lib/widgets/app_bar.dart

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,66 @@ import 'store.dart';
66
///
77
/// This should be used for most of the pages with access to [PerAccountStore].
88
class ZulipAppBar extends AppBar {
9+
/// Creates our Zulip custom app bar based on [AppBar].
10+
///
11+
/// [buildTitle] is passed a boolean `willCenterTitle` that answers
12+
/// whether the underlying [AppBar] will decide to center [title]
13+
/// based on [centerTitle], the theme, the platform, and [actions].
14+
/// Useful if [title] is a container whose children should align the same way,
15+
/// such as a [Column] with multiple lines of text.
16+
// TODO(upstream) send a PR to replace our `willCenterTitle` code
917
ZulipAppBar({
1018
super.key,
1119
super.titleSpacing,
12-
required super.title,
20+
Widget? title,
21+
Widget Function(bool willCenterTitle)? buildTitle,
22+
super.centerTitle,
1323
super.backgroundColor,
1424
super.shape,
1525
super.actions,
16-
}) : super(bottom: _ZulipAppBarBottom(backgroundColor: backgroundColor));
26+
}) :
27+
assert((title == null) != (buildTitle == null)),
28+
super(
29+
bottom: _ZulipAppBarBottom(backgroundColor: backgroundColor),
30+
title: title ?? _Title(centerTitle: centerTitle, actions: actions, buildTitle: buildTitle!)
31+
);
32+
}
33+
34+
class _Title extends StatelessWidget {
35+
const _Title({
36+
required this.centerTitle,
37+
required this.actions,
38+
required this.buildTitle,
39+
});
40+
41+
final bool? centerTitle;
42+
final List<Widget>? actions;
43+
final Widget Function(bool centerTitle) buildTitle;
44+
45+
// A copy of [AppBar._getEffectiveCenterTitle].
46+
bool _getEffectiveCenterTitle(ThemeData theme) {
47+
bool platformCenter() {
48+
switch (theme.platform) {
49+
case TargetPlatform.android:
50+
case TargetPlatform.fuchsia:
51+
case TargetPlatform.linux:
52+
case TargetPlatform.windows:
53+
return false;
54+
case TargetPlatform.iOS:
55+
case TargetPlatform.macOS:
56+
return actions == null || actions!.length < 2;
57+
}
58+
}
59+
60+
return centerTitle ?? theme.appBarTheme.centerTitle ?? platformCenter();
61+
}
62+
63+
@override
64+
Widget build(BuildContext context) {
65+
final theme = Theme.of(context);
66+
final willCenterTitle = _getEffectiveCenterTitle(theme);
67+
return buildTitle(willCenterTitle);
68+
}
1769
}
1870

1971
class _ZulipAppBarBottom extends StatelessWidget implements PreferredSizeWidget {

lib/widgets/message_list.dart

Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -269,8 +269,6 @@ class _MessageListPageState extends State<MessageListPage> implements MessageLis
269269

270270
List<Widget>? actions;
271271
if (narrow case TopicNarrow(:final streamId)) {
272-
// The helper [_getEffectiveCenterTitle] relies on the fact that we
273-
// have at most one action here.
274272
(actions ??= []).add(IconButton(
275273
icon: const Icon(ZulipIcons.message_feed),
276274
tooltip: zulipLocalizations.channelFeedButtonTooltip,
@@ -281,7 +279,8 @@ class _MessageListPageState extends State<MessageListPage> implements MessageLis
281279

282280
return Scaffold(
283281
appBar: ZulipAppBar(
284-
title: MessageListAppBarTitle(narrow: narrow),
282+
buildTitle: (willCenterTitle) =>
283+
MessageListAppBarTitle(narrow: narrow, willCenterTitle: willCenterTitle),
285284
actions: actions,
286285
backgroundColor: appBarBackgroundColor,
287286
shape: removeAppBarBottomBorder
@@ -321,9 +320,14 @@ class _MessageListPageState extends State<MessageListPage> implements MessageLis
321320
}
322321

323322
class MessageListAppBarTitle extends StatelessWidget {
324-
const MessageListAppBarTitle({super.key, required this.narrow});
323+
const MessageListAppBarTitle({
324+
super.key,
325+
required this.narrow,
326+
required this.willCenterTitle,
327+
});
325328

326329
final Narrow narrow;
330+
final bool willCenterTitle;
327331

328332
Widget _buildStreamRow(BuildContext context, {
329333
ZulipStream? stream,
@@ -367,29 +371,6 @@ class MessageListAppBarTitle extends StatelessWidget {
367371
]);
368372
}
369373

370-
// TODO(upstream): provide an API for this
371-
// Adapted from [AppBar._getEffectiveCenterTitle].
372-
bool _getEffectiveCenterTitle(ThemeData theme) {
373-
bool platformCenter() {
374-
switch (theme.platform) {
375-
case TargetPlatform.android:
376-
case TargetPlatform.fuchsia:
377-
case TargetPlatform.linux:
378-
case TargetPlatform.windows:
379-
return false;
380-
case TargetPlatform.iOS:
381-
case TargetPlatform.macOS:
382-
// We rely on the fact that there is at most one action
383-
// on the message list app bar, so that the expression returned
384-
// in the original helper, `actions == null || actions!.length < 2`,
385-
// always evaluates to `true`:
386-
return true;
387-
}
388-
}
389-
390-
return theme.appBarTheme.centerTitle ?? platformCenter();
391-
}
392-
393374
@override
394375
Widget build(BuildContext context) {
395376
final zulipLocalizations = ZulipLocalizations.of(context);
@@ -410,19 +391,17 @@ class MessageListAppBarTitle extends StatelessWidget {
410391
return _buildStreamRow(context, stream: stream);
411392

412393
case TopicNarrow(:var streamId, :var topic):
413-
final theme = Theme.of(context);
414394
final store = PerAccountStoreWidget.of(context);
415395
final stream = store.streams[streamId];
416-
final centerTitle = _getEffectiveCenterTitle(theme);
417396
return SizedBox(
418397
width: double.infinity,
419398
child: GestureDetector(
420399
behavior: HitTestBehavior.translucent,
421400
onLongPress: () => showTopicActionSheet(context,
422401
channelId: streamId, topic: topic),
423402
child: Column(
424-
crossAxisAlignment: centerTitle ? CrossAxisAlignment.center
425-
: CrossAxisAlignment.start,
403+
crossAxisAlignment: willCenterTitle ? CrossAxisAlignment.center
404+
: CrossAxisAlignment.start,
426405
children: [
427406
_buildStreamRow(context, stream: stream),
428407
_buildTopicRow(context, stream: stream, topic: topic),

test/widgets/app_bar_test.dart

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import 'package:checks/checks.dart';
2+
import 'package:flutter/foundation.dart';
23
import 'package:flutter/material.dart';
4+
import 'package:flutter_checks/flutter_checks.dart';
35
import 'package:flutter_test/flutter_test.dart';
46
import 'package:zulip/widgets/app_bar.dart';
57
import 'package:zulip/widgets/profile.dart';
@@ -35,4 +37,124 @@ void main() {
3537
check(tester.getRect(find.byType(ZulipAppBar))).equals(rectBefore);
3638
check(finder.evaluate()).single;
3739
});
40+
41+
group("buildTitle's willCenterTitle agrees with Material AppBar", () {
42+
/// Build an [AppBar]; inspect and return whether it decided to center.
43+
Future<bool> material(WidgetTester tester, {
44+
required bool? paramValue,
45+
required bool? themeValue,
46+
required List<Widget>? actions,
47+
}) async {
48+
testBinding.reset();
49+
50+
final themeData = ThemeData(appBarTheme: AppBarTheme(centerTitle: themeValue));
51+
final widget = TestZulipApp(
52+
child: Theme(data: themeData,
53+
child: AppBar(
54+
centerTitle: paramValue,
55+
actions: actions,
56+
title: const Text('a'))));
57+
58+
await tester.pumpWidget(widget);
59+
await tester.pump();
60+
61+
// test assumes LTR text direction
62+
check(tester.platformDispatcher.locale).equals(const Locale('en', 'US'));
63+
assert(actions == null || actions.isNotEmpty);
64+
final titleAreaRightEdgeOffset = actions == null
65+
? (tester.view.physicalSize / tester.view.devicePixelRatio).width
66+
: tester.getTopLeft(find.byWidget(actions.first)).dx;
67+
final titlePosition = tester.getTopLeft(find.text('a')).dx;
68+
final isCentered = titlePosition > ((1 / 3) * titleAreaRightEdgeOffset);
69+
check(titlePosition).isLessThan((2 / 3) * titleAreaRightEdgeOffset);
70+
71+
return isCentered;
72+
}
73+
74+
/// Build a [ZulipAppBar]; return willCenterTitle from the buildTitle call.
75+
Future<bool> ours(WidgetTester tester, {
76+
required bool? paramValue,
77+
required bool? themeValue,
78+
required List<Widget>? actions,
79+
}) async {
80+
testBinding.reset();
81+
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot());
82+
83+
bool? result;
84+
85+
final widget = TestZulipApp(
86+
// ZulipAppBar expects a per-account context (for the loading indicator)
87+
accountId: eg.selfAccount.id,
88+
child: Builder(builder: (context) => Theme(
89+
data: Theme.of(context).copyWith(appBarTheme: AppBarTheme(centerTitle: themeValue)),
90+
child: ZulipAppBar(
91+
centerTitle: paramValue,
92+
actions: actions,
93+
buildTitle: (willCenterTitle) {
94+
result = willCenterTitle;
95+
return const Text('a');
96+
}))));
97+
98+
await tester.pumpWidget(widget);
99+
await tester.pump(); // global store
100+
await tester.pump(); // per-account store
101+
check(find.widgetWithText(ZulipAppBar, 'a')).findsOne();
102+
103+
check(result).isNotNull();
104+
return result!;
105+
}
106+
107+
void doTest(String description, bool expectedWillCenter, {
108+
bool? paramValue,
109+
bool? themeValue,
110+
TargetPlatform? platform,
111+
List<Widget>? actions,
112+
}) {
113+
testWidgets(description, (tester) async {
114+
addTearDown(testBinding.reset);
115+
debugDefaultTargetPlatformOverride = platform;
116+
117+
check(
118+
await ours(tester, paramValue: paramValue, themeValue: themeValue, actions: actions)
119+
)..equals(
120+
await material(tester, paramValue: paramValue, themeValue: themeValue, actions: actions)
121+
)..equals(expectedWillCenter);
122+
123+
// TODO(upstream) Do this in an addTearDown, once we can:
124+
// https://github.com/flutter/flutter/issues/123189
125+
debugDefaultTargetPlatformOverride = null;
126+
});
127+
}
128+
129+
const iOS = TargetPlatform.iOS;
130+
const android = TargetPlatform.android;
131+
132+
Widget button() => IconButton(icon: const Icon(Icons.add), onPressed: () {});
133+
final oneButton = [button()];
134+
final twoButtons = [button(), button()];
135+
final threeButtons = [button(), button(), button()];
136+
137+
doTest('ios', true, platform: iOS);
138+
doTest('android', false, platform: android);
139+
140+
doTest('ios, theme false', false, platform: iOS, themeValue: false);
141+
doTest('android, theme true', true, platform: android, themeValue: true);
142+
143+
doTest('ios, param false', false, platform: iOS, paramValue: false);
144+
doTest('android, param true', true, platform: android, paramValue: true);
145+
146+
doTest('ios, theme true, param false', false, platform: iOS, themeValue: true, paramValue: false);
147+
doTest('ios, theme false, param true', true, platform: iOS, themeValue: false, paramValue: true);
148+
149+
doTest('android, theme true, param false', false, platform: android, themeValue: true, paramValue: false);
150+
doTest('android, theme false, param true', true, platform: android, themeValue: false, paramValue: true);
151+
152+
doTest('ios, no actions', true, platform: iOS, actions: null);
153+
doTest('ios, one action', true, platform: iOS, actions: oneButton);
154+
doTest('ios, two actions' , false, platform: iOS, actions: twoButtons);
155+
doTest('ios, three actions', false, platform: iOS, actions: threeButtons);
156+
157+
doTest('ios, two actions but param true', true, platform: iOS, paramValue: true, actions: twoButtons);
158+
doTest('ios, two actions but theme true', true, platform: iOS, themeValue: true, actions: twoButtons);
159+
});
38160
}

0 commit comments

Comments
 (0)