Skip to content

Commit 0e99b76

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 dbc7542 commit 0e99b76

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,12 +110,15 @@ 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
labelEdited: const HSLColor.fromAHSL(0.35, 0, 0, 0).toColor(),
116116
labelMenuButton: const Color(0xff222222),
117117
mainBackground: const Color(0xfff0f0f0),
118118
title: const Color(0xff1a1a1a),
119+
bgComposeBox: const Color(0xffffffff),
120+
textInput: const Color(0xff000000),
121+
foreground: const Color(0xff000000),
119122
channelColorSwatches: ChannelColorSwatches.light,
120123
atMentionMarker: const HSLColor.fromAHSL(0.5, 0, 0, 0.2).toColor(),
121124
dmHeaderBg: const HSLColor.fromAHSL(1, 46, 0.35, 0.93).toColor(),
@@ -139,12 +142,15 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
139142
bgCounterUnread: const Color(0xff666699).withValues(alpha: 0.37),
140143
bgTopBar: const Color(0xff242424),
141144
borderBar: Colors.black.withValues(alpha: 0.41),
142-
icon: const Color(0xff7070c2),
145+
icon: const Color(0xff7977fe),
143146
labelCounterUnread: const Color(0xffffffff).withValues(alpha: 0.7),
144147
labelEdited: const HSLColor.fromAHSL(0.35, 0, 0, 1).toColor(),
145148
labelMenuButton: const Color(0xffffffff).withValues(alpha: 0.85),
146149
mainBackground: const Color(0xff1d1d1d),
147150
title: const Color(0xffffffff),
151+
bgComposeBox: const Color(0xff0f0f0f),
152+
textInput: const Color(0xffffffff).withValues(alpha: 0.9),
153+
foreground: const Color(0xffffffff),
148154
channelColorSwatches: ChannelColorSwatches.dark,
149155
// TODO(design-dark) need proper dark-theme color (this is ad hoc)
150156
atMentionMarker: const HSLColor.fromAHSL(0.4, 0, 0, 1).toColor(),
@@ -180,6 +186,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
180186
required this.labelMenuButton,
181187
required this.mainBackground,
182188
required this.title,
189+
required this.bgComposeBox,
190+
required this.textInput,
191+
required this.foreground,
183192
required this.channelColorSwatches,
184193
required this.atMentionMarker,
185194
required this.dmHeaderBg,
@@ -217,6 +226,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
217226
final Color labelMenuButton;
218227
final Color mainBackground;
219228
final Color title;
229+
final Color bgComposeBox;
230+
final Color textInput;
231+
final Color foreground;
220232

221233
// Not exactly from the Figma design, but from Vlad anyway.
222234
final ChannelColorSwatches channelColorSwatches;
@@ -249,6 +261,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
249261
Color? labelMenuButton,
250262
Color? mainBackground,
251263
Color? title,
264+
Color? bgComposeBox,
265+
Color? textInput,
266+
Color? foreground,
252267
ChannelColorSwatches? channelColorSwatches,
253268
Color? atMentionMarker,
254269
Color? dmHeaderBg,
@@ -276,6 +291,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
276291
labelMenuButton: labelMenuButton ?? this.labelMenuButton,
277292
mainBackground: mainBackground ?? this.mainBackground,
278293
title: title ?? this.title,
294+
bgComposeBox: bgComposeBox ?? this.bgComposeBox,
295+
textInput: textInput ?? this.textInput,
296+
foreground: foreground ?? this.foreground,
279297
channelColorSwatches: channelColorSwatches ?? this.channelColorSwatches,
280298
atMentionMarker: atMentionMarker ?? this.atMentionMarker,
281299
dmHeaderBg: dmHeaderBg ?? this.dmHeaderBg,
@@ -310,6 +328,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
310328
labelMenuButton: Color.lerp(labelMenuButton, other.labelMenuButton, t)!,
311329
mainBackground: Color.lerp(mainBackground, other.mainBackground, t)!,
312330
title: Color.lerp(title, other.title, t)!,
331+
bgComposeBox: Color.lerp(bgComposeBox, other.bgComposeBox, t)!,
332+
textInput: Color.lerp(textInput, other.textInput, t)!,
333+
foreground: Color.lerp(foreground, other.foreground, t)!,
313334
channelColorSwatches: ChannelColorSwatches.lerp(channelColorSwatches, other.channelColorSwatches, t),
314335
atMentionMarker: Color.lerp(atMentionMarker, other.atMentionMarker, t)!,
315336
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
@@ -16,6 +16,7 @@ import 'package:zulip/model/narrow.dart';
1616
import 'package:zulip/model/store.dart';
1717
import 'package:zulip/widgets/compose_box.dart';
1818
import 'package:zulip/widgets/icons.dart';
19+
import 'package:zulip/widgets/theme.dart';
1920

2021
import '../api/fake_api.dart';
2122
import '../example_data.dart' as eg;
@@ -256,10 +257,10 @@ void main() {
256257
of: find.byIcon(ZulipIcons.send),
257258
matching: find.byType(IconButton)));
258259
final sendButtonWidget = sendButtonElement.widget as IconButton;
259-
final colorScheme = Theme.of(sendButtonElement).colorScheme;
260+
final designVariables = DesignVariables.of(sendButtonElement);
260261
final expectedForegroundColor = expected
261-
? colorScheme.onSurface.withValues(alpha: 0.38)
262-
: colorScheme.onPrimary;
262+
? designVariables.icon.withValues(alpha: 0.5)
263+
: designVariables.icon;
263264
check(sendButtonWidget.color).isNotNull().isSameColorAs(expectedForegroundColor);
264265
}
265266

0 commit comments

Comments
 (0)