Skip to content

Commit 6a798f7

Browse files
committed
FIX LINK WHEN MERGING 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 fd3f530 commit 6a798f7

File tree

3 files changed

+134
-112
lines changed

3 files changed

+134
-112
lines changed

lib/widgets/compose_box.dart

+107-107
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,43 @@ 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 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: ClipRect(
299300
child: ComposeAutocomplete(
300301
narrow: narrow,
301302
controller: controller,
302303
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-
));
304+
fieldViewBuilder: (context) => TextField(
305+
controller: controller,
306+
focusNode: focusNode,
307+
// `contentPadding` causes the text to be clipped while leaving
308+
// a gap to the top border, because it shrinks the size of the
309+
// body of `TextField`. Overriding this gives us full control
310+
// over the clipping behavior with the `ConstrainedBox`.
311+
clipBehavior: Clip.none,
312+
minLines: 2,
313+
maxLines: null,
314+
textCapitalization: TextCapitalization.sentences,
315+
style: TextStyle(
316+
fontSize: 17,
317+
height: (contentLineHeight / 17),
318+
color: designVariables.textInput),
319+
decoration: InputDecoration(
320+
isDense: true,
321+
border: InputBorder.none,
322+
contentPadding: const EdgeInsets.only(top: topPadding),
323+
hintText: hintText,
324+
hintStyle: TextStyle(
325+
color: designVariables.textInput.withValues(alpha: 0.5)))))));
314326
}
315327
}
316328

@@ -391,20 +403,39 @@ class _TopicInput extends StatelessWidget {
391403

392404
@override
393405
Widget build(BuildContext context) {
406+
const textFieldHeight = 42;
394407
final zulipLocalizations = ZulipLocalizations.of(context);
395-
ColorScheme colorScheme = Theme.of(context).colorScheme;
408+
final designVariables = DesignVariables.of(context);
409+
TextStyle topicTextStyle = TextStyle(
410+
fontSize: 22,
411+
height: textFieldHeight / 22,
412+
color: designVariables.textInput,
413+
).merge(weightVariableTextStyle(context, wght: 600));
396414

397415
return TopicAutocomplete(
398416
streamId: streamId,
399417
controller: controller,
400418
focusNode: focusNode,
401419
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),
420+
fieldViewBuilder: (context) => Stack(
421+
children: [
422+
TextField(
423+
controller: controller,
424+
focusNode: focusNode,
425+
textInputAction: TextInputAction.next,
426+
style: topicTextStyle,
427+
decoration: InputDecoration(
428+
isDense: true,
429+
border: InputBorder.none,
430+
hintText: zulipLocalizations.composeBoxTopicHintText,
431+
hintStyle: topicTextStyle.copyWith(
432+
color: designVariables.textInput.withValues(alpha: 0.5)))),
433+
Positioned(bottom: 0, left: 0, right: 0,
434+
child: Container(height: 1, decoration: BoxDecoration(
435+
border: Border(
436+
bottom: BorderSide(width: 1,
437+
color: designVariables.foreground.withValues(alpha: 0.2)))))),
438+
],
408439
));
409440
}
410441
}
@@ -578,10 +609,13 @@ abstract class _AttachUploadsButton extends StatelessWidget {
578609
@override
579610
Widget build(BuildContext context) {
580611
final zulipLocalizations = ZulipLocalizations.of(context);
581-
return IconButton(
582-
icon: Icon(icon),
583-
tooltip: tooltip(zulipLocalizations),
584-
onPressed: () => _handlePress(context));
612+
return SizedBox(
613+
width: _composeButtonWidth,
614+
child: IconButton(
615+
icon: Icon(icon),
616+
tooltip: tooltip(zulipLocalizations),
617+
onPressed: () => _handlePress(context),
618+
style: const ButtonStyle(splashFactory: NoSplash.splashFactory)));
585619
}
586620
}
587621

@@ -841,39 +875,20 @@ class _SendButtonState extends State<_SendButton> {
841875

842876
@override
843877
Widget build(BuildContext context) {
844-
final disabled = _hasValidationErrors;
845-
final colorScheme = Theme.of(context).colorScheme;
878+
final designVariables = DesignVariables.of(context);
846879
final zulipLocalizations = ZulipLocalizations.of(context);
847880

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-
),
881+
return SizedBox(
882+
width: _composeButtonWidth,
863883
child: IconButton(
864884
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,
885+
color: _hasValidationErrors
886+
// TODO(design): need send button color when disabled
887+
? designVariables.icon.withValues(alpha: 0.5)
888+
: designVariables.icon,
875889
icon: const Icon(ZulipIcons.send),
876-
onPressed: _send));
890+
onPressed: _send,
891+
style: const ButtonStyle(splashFactory: NoSplash.splashFactory)));
877892
}
878893
}
879894

@@ -884,18 +899,16 @@ class _ComposeBoxContainer extends StatelessWidget {
884899

885900
@override
886901
Widget build(BuildContext context) {
887-
ColorScheme colorScheme = Theme.of(context).colorScheme;
902+
final designVariables = DesignVariables.of(context);
888903

889904
// TODO(design): Maybe put a max width on the compose box, like we do on
890905
// the message list itself
891-
return SizedBox(width: double.infinity,
906+
return Container(width: double.infinity,
907+
decoration: BoxDecoration(
908+
border: Border(top: BorderSide(color: designVariables.borderBar))),
892909
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))));
910+
color: designVariables.bgComposeBox,
911+
child: SafeArea(child: child)));
899912
}
900913
}
901914

@@ -916,45 +929,32 @@ class _ComposeBoxLayout extends StatelessWidget {
916929

917930
@override
918931
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-
);
932+
final themeData = Theme.of(context);
933+
final designVariables = DesignVariables.of(context);
935934

936935
return _ComposeBoxContainer(
937936
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-
])),
937+
if (topicInput != null)
938+
Padding(padding: const EdgeInsets.symmetric(horizontal: 16),
939+
child: topicInput!),
940+
Padding(padding: const EdgeInsets.symmetric(horizontal: 16),
941+
child: contentInput),
942+
Container(
943+
padding: const EdgeInsets.symmetric(horizontal: 8),
944+
height: _composeButtonHeight,
945+
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween,
946+
children: [
947+
Theme(
948+
data: themeData.copyWith(
949+
iconTheme: themeData.iconTheme.copyWith(
950+
color: designVariables.foreground.withValues(alpha: 0.5))),
951+
child: Row(children: [
952+
_AttachFileButton(contentController: contentController, contentFocusNode: contentFocusNode),
953+
_AttachMediaButton(contentController: contentController, contentFocusNode: contentFocusNode),
954+
_AttachFromCameraButton(contentController: contentController, contentFocusNode: contentFocusNode),
955+
])),
956+
sendButton,
957+
])),
958958
]));
959959
}
960960
}

lib/widgets/theme.dart

+23-2
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

+4-3
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)