Skip to content

Commit 9c81498

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) - The 8px padding before the content input is skipped in this implementation. (See https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=3954-14242&node-type=FRAME&t=j3dQ4j3qhi5FYIQC-0) We could wrap the TextField in _ContentInput in a SingleChildScrollView widget, but then some specific EditableText scrolling behaviors (like hiding the cursor when it is scrolled out of view) will no longer work. 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 82d07ce commit 9c81498

File tree

3 files changed

+129
-114
lines changed

3 files changed

+129
-114
lines changed

lib/widgets/compose_box.dart

+102-109
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,33 @@ 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,
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) => TextField(
301304
controller: controller,
302305
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-
));
306+
decoration: InputDecoration.collapsed(
307+
hintText: hintText,
308+
hintStyle: TextStyle(color: designVariables.textInput.withValues(alpha: 0.5))),
309+
minLines: 2,
310+
maxLines: null,
311+
textCapitalization: TextCapitalization.sentences,
312+
style: TextStyle(
313+
fontSize: 17,
314+
height: (contentLineHeight / 17),
315+
color: designVariables.textInput))));
314316
}
315317
}
316318

@@ -391,20 +393,42 @@ class _TopicInput extends StatelessWidget {
391393

392394
@override
393395
Widget build(BuildContext context) {
396+
const textFieldHeight = 42;
397+
const lineHeight = 22;
394398
final zulipLocalizations = ZulipLocalizations.of(context);
395-
ColorScheme colorScheme = Theme.of(context).colorScheme;
399+
final designVariables = DesignVariables.of(context);
400+
TextStyle topicTextStyle = TextStyle(
401+
fontSize: 22,
402+
height: lineHeight / 22,
403+
color: designVariables.textInput,
404+
).merge(weightVariableTextStyle(context, wght: 600));
396405

397406
return TopicAutocomplete(
398407
streamId: streamId,
399408
controller: controller,
400409
focusNode: focusNode,
401410
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),
411+
fieldViewBuilder: (context) => Stack(
412+
children: [
413+
TextField(
414+
controller: controller,
415+
focusNode: focusNode,
416+
textInputAction: TextInputAction.next,
417+
style: topicTextStyle,
418+
decoration: InputDecoration(
419+
isDense: true,
420+
contentPadding: const EdgeInsets.symmetric(
421+
vertical: (textFieldHeight - lineHeight) / 2),
422+
border: InputBorder.none,
423+
hintText: zulipLocalizations.composeBoxTopicHintText,
424+
hintStyle: topicTextStyle.copyWith(
425+
color: designVariables.textInput.withValues(alpha: 0.5)))),
426+
Positioned(bottom: 0, left: 0, right: 0,
427+
child: Container(height: 1, decoration: BoxDecoration(
428+
border: Border(
429+
bottom: BorderSide(width: 1,
430+
color: designVariables.foreground.withValues(alpha: 0.2)))))),
431+
],
408432
));
409433
}
410434
}
@@ -578,10 +602,13 @@ abstract class _AttachUploadsButton extends StatelessWidget {
578602
@override
579603
Widget build(BuildContext context) {
580604
final zulipLocalizations = ZulipLocalizations.of(context);
581-
return IconButton(
582-
icon: Icon(icon),
583-
tooltip: tooltip(zulipLocalizations),
584-
onPressed: () => _handlePress(context));
605+
return SizedBox(
606+
width: _composeButtonWidth,
607+
child: IconButton(
608+
icon: Icon(icon),
609+
tooltip: tooltip(zulipLocalizations),
610+
onPressed: () => _handlePress(context),
611+
style: const ButtonStyle(splashFactory: NoSplash.splashFactory)));
585612
}
586613
}
587614

@@ -841,39 +868,20 @@ class _SendButtonState extends State<_SendButton> {
841868

842869
@override
843870
Widget build(BuildContext context) {
844-
final disabled = _hasValidationErrors;
845-
final colorScheme = Theme.of(context).colorScheme;
871+
final designVariables = DesignVariables.of(context);
846872
final zulipLocalizations = ZulipLocalizations.of(context);
847873

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-
),
874+
return SizedBox(
875+
width: _composeButtonWidth,
863876
child: IconButton(
864877
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,
878+
color: _hasValidationErrors
879+
// TODO(design): need send button color when disabled
880+
? designVariables.icon.withValues(alpha: 0.5)
881+
: designVariables.icon,
875882
icon: const Icon(ZulipIcons.send),
876-
onPressed: _send));
883+
onPressed: _send,
884+
style: const ButtonStyle(splashFactory: NoSplash.splashFactory)));
877885
}
878886
}
879887

@@ -884,18 +892,16 @@ class _ComposeBoxContainer extends StatelessWidget {
884892

885893
@override
886894
Widget build(BuildContext context) {
887-
ColorScheme colorScheme = Theme.of(context).colorScheme;
895+
final designVariables = DesignVariables.of(context);
888896

889897
// TODO(design): Maybe put a max width on the compose box, like we do on
890898
// the message list itself
891-
return SizedBox(width: double.infinity,
899+
return Container(width: double.infinity,
900+
decoration: BoxDecoration(
901+
border: Border(top: BorderSide(color: designVariables.borderBar))),
892902
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))));
903+
color: designVariables.bgComposeBox,
904+
child: SafeArea(child: child)));
899905
}
900906
}
901907

@@ -916,45 +922,32 @@ class _ComposeBoxLayout extends StatelessWidget {
916922

917923
@override
918924
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-
);
925+
final themeData = Theme.of(context);
926+
final designVariables = DesignVariables.of(context);
935927

936928
return _ComposeBoxContainer(
937929
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-
])),
930+
if (topicInput != null)
931+
Padding(padding: const EdgeInsets.symmetric(horizontal: 16),
932+
child: topicInput!),
933+
Padding(padding: const EdgeInsets.symmetric(horizontal: 16),
934+
child: contentInput),
935+
Container(
936+
padding: const EdgeInsets.symmetric(horizontal: 8),
937+
height: _composeButtonHeight,
938+
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween,
939+
children: [
940+
Theme(
941+
data: themeData.copyWith(
942+
iconTheme: themeData.iconTheme.copyWith(
943+
color: designVariables.foreground.withValues(alpha: 0.5))),
944+
child: Row(children: [
945+
_AttachFileButton(contentController: contentController, contentFocusNode: contentFocusNode),
946+
_AttachMediaButton(contentController: contentController, contentFocusNode: contentFocusNode),
947+
_AttachFromCameraButton(contentController: contentController, contentFocusNode: contentFocusNode),
948+
])),
949+
sendButton,
950+
])),
958951
]));
959952
}
960953
}

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)