@@ -17,10 +17,11 @@ import 'autocomplete.dart';
17
17
import 'dialog.dart' ;
18
18
import 'icons.dart' ;
19
19
import 'store.dart' ;
20
+ import 'text.dart' ;
20
21
import 'theme.dart' ;
21
22
22
- const double _inputVerticalPadding = 8 ;
23
- const double _sendButtonSize = 36 ;
23
+ const double _composeButtonWidth = 44 ;
24
+ const double _composeButtonHeight = 42 ;
24
25
25
26
/// A [TextEditingController] for use in the compose box.
26
27
///
@@ -285,32 +286,43 @@ class _ContentInput extends StatelessWidget {
285
286
286
287
@override
287
288
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 (
299
300
child: ComposeAutocomplete (
300
301
narrow: narrow,
301
302
controller: controller,
302
303
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 )))))));
314
326
}
315
327
}
316
328
@@ -391,20 +403,39 @@ class _TopicInput extends StatelessWidget {
391
403
392
404
@override
393
405
Widget build (BuildContext context) {
406
+ const textFieldHeight = 42 ;
394
407
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 ));
396
414
397
415
return TopicAutocomplete (
398
416
streamId: streamId,
399
417
controller: controller,
400
418
focusNode: focusNode,
401
419
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
+ ],
408
439
));
409
440
}
410
441
}
@@ -578,10 +609,13 @@ abstract class _AttachUploadsButton extends StatelessWidget {
578
609
@override
579
610
Widget build (BuildContext context) {
580
611
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)));
585
619
}
586
620
}
587
621
@@ -841,39 +875,20 @@ class _SendButtonState extends State<_SendButton> {
841
875
842
876
@override
843
877
Widget build (BuildContext context) {
844
- final disabled = _hasValidationErrors;
845
- final colorScheme = Theme .of (context).colorScheme;
878
+ final designVariables = DesignVariables .of (context);
846
879
final zulipLocalizations = ZulipLocalizations .of (context);
847
880
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,
863
883
child: IconButton (
864
884
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,
875
889
icon: const Icon (ZulipIcons .send),
876
- onPressed: _send));
890
+ onPressed: _send,
891
+ style: const ButtonStyle (splashFactory: NoSplash .splashFactory)));
877
892
}
878
893
}
879
894
@@ -884,18 +899,16 @@ class _ComposeBoxContainer extends StatelessWidget {
884
899
885
900
@override
886
901
Widget build (BuildContext context) {
887
- ColorScheme colorScheme = Theme .of (context).colorScheme ;
902
+ final designVariables = DesignVariables .of (context);
888
903
889
904
// TODO(design): Maybe put a max width on the compose box, like we do on
890
905
// 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))),
892
909
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)));
899
912
}
900
913
}
901
914
@@ -916,45 +929,32 @@ class _ComposeBoxLayout extends StatelessWidget {
916
929
917
930
@override
918
931
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);
935
934
936
935
return _ComposeBoxContainer (
937
936
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
+ ])),
958
958
]));
959
959
}
960
960
}
0 commit comments