Skip to content

Commit 9ee3ed0

Browse files
committed
FIX LINK WHEN MERGING compose_box: Support the redesigned layout for the compose box.
Note: - The ButtonStyle for the send button was added in # 399, to fix a sizing issue irrelevant to the new design. - All the design variables come from the Figma design. Among them, DesignVariables.icon gets used for the first time in this commit, and its value has been updated to match the current design. - We removed all the splash effects for buttons. (See https://github.com/zulip/zulip-flutter/pull/ 853#discussion_r1720334991) See also: - https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=3954-13395 - https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=3862-14350
1 parent f82cae5 commit 9ee3ed0

File tree

3 files changed

+133
-112
lines changed

3 files changed

+133
-112
lines changed

lib/widgets/compose_box.dart

Lines changed: 106 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ import 'autocomplete.dart';
1717
import 'dialog.dart';
1818
import 'icons.dart';
1919
import 'store.dart';
20+
import 'text.dart';
2021
import 'theme.dart';
2122

22-
const double _inputVerticalPadding = 8;
23-
const double _sendButtonSize = 36;
23+
const double _composeButtonWidth = 44;
24+
const double _composeButtonHeight = 42;
2425

2526
/// A [TextEditingController] for use in the compose box.
2627
///
@@ -285,32 +286,39 @@ class _ContentInput extends StatelessWidget {
285286

286287
@override
287288
Widget build(BuildContext context) {
288-
ColorScheme colorScheme = Theme.of(context).colorScheme;
289-
290-
return InputDecorator(
291-
decoration: const InputDecoration(),
292-
child: ConstrainedBox(
293-
constraints: const BoxConstraints(
294-
minHeight: _sendButtonSize - 2 * _inputVerticalPadding,
295-
296-
// TODO constrain this adaptively (i.e. not hard-coded 200)
297-
maxHeight: 200,
298-
),
299-
child: ComposeAutocomplete(
300-
narrow: narrow,
301-
controller: controller,
302-
focusNode: focusNode,
303-
fieldViewBuilder: (context) {
304-
return TextField(
289+
final designVariables = DesignVariables.of(context);
290+
const topPadding = 8.0;
291+
const contentLineHeight = 22.0;
292+
293+
return ConstrainedBox(
294+
constraints: const BoxConstraints(
295+
// Reserve space to fully show the first 7th lines and just partially
296+
// clip the 8th line, where the height matches the spec of 178 logical
297+
// pixels. The partial line hints that the content input is scrollable.
298+
maxHeight: topPadding + contentLineHeight * 7 + contentLineHeight * 0.727),
299+
child: ComposeAutocomplete(
300+
narrow: narrow,
301+
controller: controller,
302+
focusNode: focusNode,
303+
fieldViewBuilder: (context) => SingleChildScrollView(
304+
// While the [TextField] is scrollable, we need to wrap it with
305+
// [SingleChildScrollView] to prepend a fixed-height padding that can
306+
// be scrolled along with the text.
307+
child: Padding(
308+
padding: const EdgeInsets.only(top: topPadding),
309+
child: TextField(
305310
controller: controller,
306311
focusNode: focusNode,
307-
style: TextStyle(color: colorScheme.onSurface),
308-
decoration: InputDecoration.collapsed(hintText: hintText),
312+
decoration: InputDecoration.collapsed(
313+
hintText: hintText,
314+
hintStyle: TextStyle(color: designVariables.textInput.withOpacity(0.5))),
315+
minLines: 2,
309316
maxLines: null,
310317
textCapitalization: TextCapitalization.sentences,
311-
);
312-
}),
313-
));
318+
style: TextStyle(
319+
fontSize: 17,
320+
height: (contentLineHeight / 17),
321+
color: designVariables.textInput))))));
314322
}
315323
}
316324

@@ -391,20 +399,42 @@ class _TopicInput extends StatelessWidget {
391399

392400
@override
393401
Widget build(BuildContext context) {
402+
const textFieldHeight = 42;
403+
const lineHeight = 22;
394404
final zulipLocalizations = ZulipLocalizations.of(context);
395-
ColorScheme colorScheme = Theme.of(context).colorScheme;
405+
final designVariables = DesignVariables.of(context);
406+
TextStyle topicTextStyle = TextStyle(
407+
fontSize: 22,
408+
height: lineHeight / 22,
409+
color: designVariables.textInput,
410+
).merge(weightVariableTextStyle(context, wght: 600));
396411

397412
return TopicAutocomplete(
398413
streamId: streamId,
399414
controller: controller,
400415
focusNode: focusNode,
401416
contentFocusNode: contentFocusNode,
402-
fieldViewBuilder: (context) => TextField(
403-
controller: controller,
404-
focusNode: focusNode,
405-
textInputAction: TextInputAction.next,
406-
style: TextStyle(color: colorScheme.onSurface),
407-
decoration: InputDecoration(hintText: zulipLocalizations.composeBoxTopicHintText),
417+
fieldViewBuilder: (context) => Stack(
418+
children: [
419+
TextField(
420+
controller: controller,
421+
focusNode: focusNode,
422+
textInputAction: TextInputAction.next,
423+
style: topicTextStyle,
424+
decoration: InputDecoration(
425+
isDense: true,
426+
contentPadding: const EdgeInsets.symmetric(
427+
vertical: (textFieldHeight - lineHeight) / 2),
428+
border: InputBorder.none,
429+
hintText: zulipLocalizations.composeBoxTopicHintText,
430+
hintStyle: topicTextStyle.copyWith(
431+
color: designVariables.textInput.withOpacity(0.5)))),
432+
Positioned(bottom: 0, left: 0, right: 0,
433+
child: Container(height: 1, decoration: BoxDecoration(
434+
border: Border(
435+
bottom: BorderSide(width: 1,
436+
color: designVariables.foreground.withOpacity(0.2)))))),
437+
],
408438
));
409439
}
410440
}
@@ -578,10 +608,13 @@ abstract class _AttachUploadsButton extends StatelessWidget {
578608
@override
579609
Widget build(BuildContext context) {
580610
final zulipLocalizations = ZulipLocalizations.of(context);
581-
return IconButton(
582-
icon: Icon(icon),
583-
tooltip: tooltip(zulipLocalizations),
584-
onPressed: () => _handlePress(context));
611+
return SizedBox(
612+
width: _composeButtonWidth,
613+
child: IconButton(
614+
icon: Icon(icon),
615+
tooltip: tooltip(zulipLocalizations),
616+
onPressed: () => _handlePress(context),
617+
style: const ButtonStyle(splashFactory: NoSplash.splashFactory)));
585618
}
586619
}
587620

@@ -841,39 +874,20 @@ class _SendButtonState extends State<_SendButton> {
841874

842875
@override
843876
Widget build(BuildContext context) {
844-
final disabled = _hasValidationErrors;
845-
final colorScheme = Theme.of(context).colorScheme;
877+
final designVariables = DesignVariables.of(context);
846878
final zulipLocalizations = ZulipLocalizations.of(context);
847879

848-
// Copy FilledButton defaults (_FilledButtonDefaultsM3.backgroundColor)
849-
final backgroundColor = disabled
850-
? colorScheme.onSurface.withOpacity(0.12)
851-
: colorScheme.primary;
852-
853-
// Copy FilledButton defaults (_FilledButtonDefaultsM3.foregroundColor)
854-
final foregroundColor = disabled
855-
? colorScheme.onSurface.withOpacity(0.38)
856-
: colorScheme.onPrimary;
857-
858-
return Ink(
859-
decoration: BoxDecoration(
860-
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
861-
color: backgroundColor,
862-
),
880+
return SizedBox(
881+
width: _composeButtonWidth,
863882
child: IconButton(
864883
tooltip: zulipLocalizations.composeBoxSendTooltip,
865-
style: const ButtonStyle(
866-
// Match the height of the content input.
867-
minimumSize: WidgetStatePropertyAll(Size.square(_sendButtonSize)),
868-
// With the default of [MaterialTapTargetSize.padded], not just the
869-
// tap target but the visual button would get padded to 48px square.
870-
// It would be nice if the tap target extended invisibly out from the
871-
// button, to make a 48px square, but that's not the behavior we get.
872-
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
873-
),
874-
color: foregroundColor,
884+
color: _hasValidationErrors
885+
// TODO(design): need send button color when disabled
886+
? designVariables.icon.withOpacity(0.5)
887+
: designVariables.icon,
875888
icon: const Icon(ZulipIcons.send),
876-
onPressed: _send));
889+
onPressed: _send,
890+
style: const ButtonStyle(splashFactory: NoSplash.splashFactory)));
877891
}
878892
}
879893

@@ -884,18 +898,16 @@ class _ComposeBoxContainer extends StatelessWidget {
884898

885899
@override
886900
Widget build(BuildContext context) {
887-
ColorScheme colorScheme = Theme.of(context).colorScheme;
901+
final designVariables = DesignVariables.of(context);
888902

889903
// TODO(design): Maybe put a max width on the compose box, like we do on
890904
// the message list itself
891-
return SizedBox(width: double.infinity,
905+
return Container(width: double.infinity,
906+
decoration: BoxDecoration(
907+
border: Border(top: BorderSide(color: designVariables.borderBar))),
892908
child: Material(
893-
color: colorScheme.surfaceContainerHighest,
894-
child: SafeArea(
895-
minimum: const EdgeInsets.fromLTRB(8, 0, 8, 8),
896-
child: Padding(
897-
padding: const EdgeInsets.only(top: 8.0),
898-
child: child))));
909+
color: designVariables.bgComposeBox,
910+
child: SafeArea(child: child)));
899911
}
900912
}
901913

@@ -916,45 +928,32 @@ class _ComposeBoxLayout extends StatelessWidget {
916928

917929
@override
918930
Widget build(BuildContext context) {
919-
ThemeData themeData = Theme.of(context);
920-
ColorScheme colorScheme = themeData.colorScheme;
921-
922-
final inputThemeData = themeData.copyWith(
923-
inputDecorationTheme: InputDecorationTheme(
924-
// Both [contentPadding] and [isDense] combine to make the layout compact.
925-
isDense: true,
926-
contentPadding: const EdgeInsets.symmetric(
927-
horizontal: 12.0, vertical: _inputVerticalPadding),
928-
border: const OutlineInputBorder(
929-
borderRadius: BorderRadius.all(Radius.circular(4.0)),
930-
borderSide: BorderSide.none),
931-
filled: true,
932-
fillColor: colorScheme.surface,
933-
),
934-
);
931+
final themeData = Theme.of(context);
932+
final designVariables = DesignVariables.of(context);
935933

936934
return _ComposeBoxContainer(
937935
child: Column(children: [
938-
Row(crossAxisAlignment: CrossAxisAlignment.end, children: [
939-
Expanded(
940-
child: Theme(
941-
data: inputThemeData,
942-
child: Column(children: [
943-
if (topicInput != null) topicInput!,
944-
if (topicInput != null) const SizedBox(height: 8),
945-
contentInput,
946-
]))),
947-
const SizedBox(width: 8),
948-
sendButton,
949-
]),
950-
Theme(
951-
data: themeData.copyWith(
952-
iconTheme: themeData.iconTheme.copyWith(color: colorScheme.onSurface.withOpacity(0.5))),
953-
child: Row(children: [
954-
_AttachFileButton(contentController: contentController, contentFocusNode: contentFocusNode),
955-
_AttachMediaButton(contentController: contentController, contentFocusNode: contentFocusNode),
956-
_AttachFromCameraButton(contentController: contentController, contentFocusNode: contentFocusNode),
957-
])),
936+
if (topicInput != null)
937+
Padding(padding: const EdgeInsets.symmetric(horizontal: 16),
938+
child: topicInput!),
939+
Padding(padding: const EdgeInsets.symmetric(horizontal: 16),
940+
child: contentInput),
941+
Container(
942+
padding: const EdgeInsets.symmetric(horizontal: 8),
943+
height: _composeButtonHeight,
944+
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween,
945+
children: [
946+
Theme(
947+
data: themeData.copyWith(
948+
iconTheme: themeData.iconTheme.copyWith(
949+
color: designVariables.foreground.withOpacity(0.5))),
950+
child: Row(children: [
951+
_AttachFileButton(contentController: contentController, contentFocusNode: contentFocusNode),
952+
_AttachMediaButton(contentController: contentController, contentFocusNode: contentFocusNode),
953+
_AttachFromCameraButton(contentController: contentController, contentFocusNode: contentFocusNode),
954+
])),
955+
sendButton,
956+
])),
958957
]));
959958
}
960959
}

lib/widgets/theme.dart

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,14 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
110110
bgCounterUnread: const Color(0xff666699).withOpacity(0.15),
111111
bgTopBar: const Color(0xfff5f5f5),
112112
borderBar: const Color(0x33000000),
113-
icon: const Color(0xff666699),
113+
icon: const Color(0xff6159e1),
114114
labelCounterUnread: const Color(0xff222222),
115115
labelMenuButton: const Color(0xff222222),
116116
mainBackground: const Color(0xfff0f0f0),
117117
title: const Color(0xff1a1a1a),
118+
bgComposeBox: const Color(0xffffffff),
119+
textInput: const Color(0xff000000),
120+
foreground: const Color(0xff000000),
118121
channelColorSwatches: ChannelColorSwatches.light,
119122
atMentionMarker: const HSLColor.fromAHSL(0.5, 0, 0, 0.2).toColor(),
120123
dmHeaderBg: const HSLColor.fromAHSL(1, 46, 0.35, 0.93).toColor(),
@@ -138,11 +141,14 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
138141
bgCounterUnread: const Color(0xff666699).withOpacity(0.37),
139142
bgTopBar: const Color(0xff242424),
140143
borderBar: Colors.black.withOpacity(0.41),
141-
icon: const Color(0xff7070c2),
144+
icon: const Color(0xff7977fe),
142145
labelCounterUnread: const Color(0xffffffff).withOpacity(0.7),
143146
labelMenuButton: const Color(0xffffffff).withOpacity(0.85),
144147
mainBackground: const Color(0xff1d1d1d),
145148
title: const Color(0xffffffff),
149+
bgComposeBox: const Color(0xff0f0f0f),
150+
textInput: const Color(0xffffffff).withOpacity(0.9),
151+
foreground: const Color(0xffffffff),
146152
channelColorSwatches: ChannelColorSwatches.dark,
147153
// TODO(design-dark) need proper dark-theme color (this is ad hoc)
148154
atMentionMarker: const HSLColor.fromAHSL(0.4, 0, 0, 1).toColor(),
@@ -177,6 +183,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
177183
required this.labelMenuButton,
178184
required this.mainBackground,
179185
required this.title,
186+
required this.bgComposeBox,
187+
required this.textInput,
188+
required this.foreground,
180189
required this.channelColorSwatches,
181190
required this.atMentionMarker,
182191
required this.dmHeaderBg,
@@ -213,6 +222,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
213222
final Color labelMenuButton;
214223
final Color mainBackground;
215224
final Color title;
225+
final Color bgComposeBox;
226+
final Color textInput;
227+
final Color foreground;
216228

217229
// Not exactly from the Figma design, but from Vlad anyway.
218230
final ChannelColorSwatches channelColorSwatches;
@@ -244,6 +256,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
244256
Color? labelMenuButton,
245257
Color? mainBackground,
246258
Color? title,
259+
Color? bgComposeBox,
260+
Color? textInput,
261+
Color? foreground,
247262
ChannelColorSwatches? channelColorSwatches,
248263
Color? atMentionMarker,
249264
Color? dmHeaderBg,
@@ -270,6 +285,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
270285
labelMenuButton: labelMenuButton ?? this.labelMenuButton,
271286
mainBackground: mainBackground ?? this.mainBackground,
272287
title: title ?? this.title,
288+
bgComposeBox: bgComposeBox ?? this.bgComposeBox,
289+
textInput: textInput ?? this.textInput,
290+
foreground: foreground ?? this.foreground,
273291
channelColorSwatches: channelColorSwatches ?? this.channelColorSwatches,
274292
atMentionMarker: atMentionMarker ?? this.atMentionMarker,
275293
dmHeaderBg: dmHeaderBg ?? this.dmHeaderBg,
@@ -303,6 +321,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
303321
labelMenuButton: Color.lerp(labelMenuButton, other.labelMenuButton, t)!,
304322
mainBackground: Color.lerp(mainBackground, other.mainBackground, t)!,
305323
title: Color.lerp(title, other.title, t)!,
324+
bgComposeBox: Color.lerp(bgComposeBox, other.bgComposeBox, t)!,
325+
textInput: Color.lerp(textInput, other.textInput, t)!,
326+
foreground: Color.lerp(foreground, other.foreground, t)!,
306327
channelColorSwatches: ChannelColorSwatches.lerp(channelColorSwatches, other.channelColorSwatches, t),
307328
atMentionMarker: Color.lerp(atMentionMarker, other.atMentionMarker, t)!,
308329
dmHeaderBg: Color.lerp(dmHeaderBg, other.dmHeaderBg, t)!,

test/widgets/compose_box_test.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import 'package:zulip/model/narrow.dart';
1515
import 'package:zulip/model/store.dart';
1616
import 'package:zulip/widgets/compose_box.dart';
1717
import 'package:zulip/widgets/icons.dart';
18+
import 'package:zulip/widgets/theme.dart';
1819

1920
import '../api/fake_api.dart';
2021
import '../example_data.dart' as eg;
@@ -255,10 +256,10 @@ void main() {
255256
of: find.byIcon(ZulipIcons.send),
256257
matching: find.byType(IconButton)));
257258
final sendButtonWidget = sendButtonElement.widget as IconButton;
258-
final colorScheme = Theme.of(sendButtonElement).colorScheme;
259+
final designVariables = DesignVariables.of(sendButtonElement);
259260
final expectedForegroundColor = expected
260-
? colorScheme.onSurface.withOpacity(0.38)
261-
: colorScheme.onPrimary;
261+
? designVariables.icon.withOpacity(0.5)
262+
: designVariables.icon;
262263
check(sendButtonWidget.color).equals(expectedForegroundColor);
263264
}
264265

0 commit comments

Comments
 (0)