Skip to content

Commit b583913

Browse files
committed
FIX LINK compose_box: Support the redesigned layout for the compose box.
Notes: - 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 37e3054 commit b583913

File tree

3 files changed

+140
-112
lines changed

3 files changed

+140
-112
lines changed

lib/widgets/compose_box.dart

Lines changed: 113 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,48 @@ 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-
),
289+
final designVariables = DesignVariables.of(context);
290+
const verticalPadding = 8.0;
291+
const contentLineHeight = 22.0;
292+
293+
return ConstrainedBox(
294+
constraints: const BoxConstraints(
295+
// The minimum height fits a little more than 2 lines to match the spec
296+
// of 54 logical pixels. The bottom padding is not added because it
297+
// is not supposed to extend the compose box.
298+
minHeight: verticalPadding + contentLineHeight * 2.091,
299+
// Reserve space to fully show the first 7th lines and just partially
300+
// clip the 8th line, where the height matches the spec of 178 logical
301+
// pixels. The partial line hints that the content input is scrollable.
302+
// The bottom padding is not added because it is not supposed to extend
303+
// the compose box.
304+
maxHeight: verticalPadding + contentLineHeight * 7 + contentLineHeight * 0.727),
305+
child: ClipRect(
299306
child: ComposeAutocomplete(
300307
narrow: narrow,
301308
controller: controller,
302309
focusNode: focusNode,
303-
fieldViewBuilder: (context) {
304-
return TextField(
305-
controller: controller,
306-
focusNode: focusNode,
307-
style: TextStyle(color: colorScheme.onSurface),
308-
decoration: InputDecoration.collapsed(hintText: hintText),
309-
maxLines: null,
310-
textCapitalization: TextCapitalization.sentences,
311-
);
312-
}),
313-
));
310+
fieldViewBuilder: (context) => TextField(
311+
controller: controller,
312+
focusNode: focusNode,
313+
// `contentPadding` causes the text to be clipped while leaving
314+
// a gap to the top border, because it shrinks the size of the
315+
// body of `TextField`. Overriding this gives us full control
316+
// over the clipping behavior with the `ConstrainedBox`.
317+
clipBehavior: Clip.none,
318+
maxLines: null,
319+
textCapitalization: TextCapitalization.sentences,
320+
style: TextStyle(
321+
fontSize: 17,
322+
height: (contentLineHeight / 17),
323+
color: designVariables.textInput),
324+
decoration: InputDecoration(
325+
isDense: true,
326+
border: InputBorder.none,
327+
contentPadding: const EdgeInsets.symmetric(vertical: verticalPadding),
328+
hintText: hintText,
329+
hintStyle: TextStyle(
330+
color: designVariables.textInput.withValues(alpha: 0.5)))))));
314331
}
315332
}
316333

@@ -391,20 +408,39 @@ class _TopicInput extends StatelessWidget {
391408

392409
@override
393410
Widget build(BuildContext context) {
411+
const textFieldHeight = 42;
394412
final zulipLocalizations = ZulipLocalizations.of(context);
395-
ColorScheme colorScheme = Theme.of(context).colorScheme;
413+
final designVariables = DesignVariables.of(context);
414+
TextStyle topicTextStyle = TextStyle(
415+
fontSize: 22,
416+
height: textFieldHeight / 22,
417+
color: designVariables.textInput,
418+
).merge(weightVariableTextStyle(context, wght: 600));
396419

397420
return TopicAutocomplete(
398421
streamId: streamId,
399422
controller: controller,
400423
focusNode: focusNode,
401424
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),
425+
fieldViewBuilder: (context) => Stack(
426+
children: [
427+
TextField(
428+
controller: controller,
429+
focusNode: focusNode,
430+
textInputAction: TextInputAction.next,
431+
style: topicTextStyle,
432+
decoration: InputDecoration(
433+
isDense: true,
434+
border: InputBorder.none,
435+
hintText: zulipLocalizations.composeBoxTopicHintText,
436+
hintStyle: topicTextStyle.copyWith(
437+
color: designVariables.textInput.withValues(alpha: 0.5)))),
438+
Positioned(bottom: 0, left: 0, right: 0,
439+
child: Container(height: 1, decoration: BoxDecoration(
440+
border: Border(
441+
bottom: BorderSide(width: 1,
442+
color: designVariables.foreground.withValues(alpha: 0.2)))))),
443+
],
408444
));
409445
}
410446
}
@@ -578,10 +614,13 @@ abstract class _AttachUploadsButton extends StatelessWidget {
578614
@override
579615
Widget build(BuildContext context) {
580616
final zulipLocalizations = ZulipLocalizations.of(context);
581-
return IconButton(
582-
icon: Icon(icon),
583-
tooltip: tooltip(zulipLocalizations),
584-
onPressed: () => _handlePress(context));
617+
return SizedBox(
618+
width: _composeButtonWidth,
619+
child: IconButton(
620+
icon: Icon(icon),
621+
tooltip: tooltip(zulipLocalizations),
622+
onPressed: () => _handlePress(context),
623+
style: const ButtonStyle(splashFactory: NoSplash.splashFactory)));
585624
}
586625
}
587626

@@ -841,39 +880,20 @@ class _SendButtonState extends State<_SendButton> {
841880

842881
@override
843882
Widget build(BuildContext context) {
844-
final disabled = _hasValidationErrors;
845-
final colorScheme = Theme.of(context).colorScheme;
883+
final designVariables = DesignVariables.of(context);
846884
final zulipLocalizations = ZulipLocalizations.of(context);
847885

848-
// Copy FilledButton defaults (_FilledButtonDefaultsM3.backgroundColor)
849-
final backgroundColor = disabled
850-
? colorScheme.onSurface.withValues(alpha: 0.12)
851-
: colorScheme.primary;
852-
853-
// Copy FilledButton defaults (_FilledButtonDefaultsM3.foregroundColor)
854-
final foregroundColor = disabled
855-
? colorScheme.onSurface.withValues(alpha: 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-
),
886+
return SizedBox(
887+
width: _composeButtonWidth,
863888
child: IconButton(
864889
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,
890+
color: _hasValidationErrors
891+
// TODO(design): need send button color when disabled
892+
? designVariables.icon.withValues(alpha: 0.5)
893+
: designVariables.icon,
875894
icon: const Icon(ZulipIcons.send),
876-
onPressed: _send));
895+
onPressed: _send,
896+
style: const ButtonStyle(splashFactory: NoSplash.splashFactory)));
877897
}
878898
}
879899

@@ -884,18 +904,16 @@ class _ComposeBoxContainer extends StatelessWidget {
884904

885905
@override
886906
Widget build(BuildContext context) {
887-
ColorScheme colorScheme = Theme.of(context).colorScheme;
907+
final designVariables = DesignVariables.of(context);
888908

889909
// TODO(design): Maybe put a max width on the compose box, like we do on
890910
// the message list itself
891-
return SizedBox(width: double.infinity,
911+
return Container(width: double.infinity,
912+
decoration: BoxDecoration(
913+
border: Border(top: BorderSide(color: designVariables.borderBar))),
892914
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))));
915+
color: designVariables.bgComposeBox,
916+
child: SafeArea(child: child)));
899917
}
900918
}
901919

@@ -916,45 +934,33 @@ class _ComposeBoxLayout extends StatelessWidget {
916934

917935
@override
918936
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-
);
937+
final themeData = Theme.of(context);
938+
final designVariables = DesignVariables.of(context);
935939

936940
return _ComposeBoxContainer(
937941
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.onSurfaceVariant)),
953-
child: Row(children: [
954-
_AttachFileButton(contentController: contentController, contentFocusNode: contentFocusNode),
955-
_AttachMediaButton(contentController: contentController, contentFocusNode: contentFocusNode),
956-
_AttachFromCameraButton(contentController: contentController, contentFocusNode: contentFocusNode),
957-
])),
942+
if (topicInput != null)
943+
Padding(padding: const EdgeInsets.symmetric(horizontal: 16),
944+
child: topicInput!),
945+
Padding(padding: const EdgeInsets.symmetric(horizontal: 16),
946+
child: contentInput),
947+
Container(
948+
padding: const EdgeInsets.symmetric(horizontal: 8),
949+
height: _composeButtonHeight,
950+
child: Row(
951+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
952+
children: [
953+
Theme(
954+
data: themeData.copyWith(
955+
iconTheme: themeData.iconTheme.copyWith(
956+
color: designVariables.foreground.withValues(alpha: 0.5))),
957+
child: Row(children: [
958+
_AttachFileButton(contentController: contentController, contentFocusNode: contentFocusNode),
959+
_AttachMediaButton(contentController: contentController, contentFocusNode: contentFocusNode),
960+
_AttachFromCameraButton(contentController: contentController, contentFocusNode: contentFocusNode),
961+
])),
962+
sendButton,
963+
])),
958964
]));
959965
}
960966
}

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).withValues(alpha: 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).withValues(alpha: 0.37),
139142
bgTopBar: const Color(0xff242424),
140143
borderBar: Colors.black.withValues(alpha: 0.41),
141-
icon: const Color(0xff7070c2),
144+
icon: const Color(0xff7977fe),
142145
labelCounterUnread: const Color(0xffffffff).withValues(alpha: 0.7),
143146
labelMenuButton: const Color(0xffffffff).withValues(alpha: 0.85),
144147
mainBackground: const Color(0xff1d1d1d),
145148
title: const Color(0xffffffff),
149+
bgComposeBox: const Color(0xff0f0f0f),
150+
textInput: const Color(0xffffffff).withValues(alpha: 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.withValues(alpha: 0.38)
261-
: colorScheme.onPrimary;
261+
? designVariables.icon.withValues(alpha: 0.5)
262+
: designVariables.icon;
262263
check(sendButtonWidget.color).equals(expectedForegroundColor);
263264
}
264265

0 commit comments

Comments
 (0)