@@ -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,48 @@ 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 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 (
299
306
child: ComposeAutocomplete (
300
307
narrow: narrow,
301
308
controller: controller,
302
309
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 )))))));
314
331
}
315
332
}
316
333
@@ -391,20 +408,39 @@ class _TopicInput extends StatelessWidget {
391
408
392
409
@override
393
410
Widget build (BuildContext context) {
411
+ const textFieldHeight = 42 ;
394
412
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 ));
396
419
397
420
return TopicAutocomplete (
398
421
streamId: streamId,
399
422
controller: controller,
400
423
focusNode: focusNode,
401
424
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
+ ],
408
444
));
409
445
}
410
446
}
@@ -578,10 +614,13 @@ abstract class _AttachUploadsButton extends StatelessWidget {
578
614
@override
579
615
Widget build (BuildContext context) {
580
616
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)));
585
624
}
586
625
}
587
626
@@ -841,39 +880,20 @@ class _SendButtonState extends State<_SendButton> {
841
880
842
881
@override
843
882
Widget build (BuildContext context) {
844
- final disabled = _hasValidationErrors;
845
- final colorScheme = Theme .of (context).colorScheme;
883
+ final designVariables = DesignVariables .of (context);
846
884
final zulipLocalizations = ZulipLocalizations .of (context);
847
885
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,
863
888
child: IconButton (
864
889
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,
875
894
icon: const Icon (ZulipIcons .send),
876
- onPressed: _send));
895
+ onPressed: _send,
896
+ style: const ButtonStyle (splashFactory: NoSplash .splashFactory)));
877
897
}
878
898
}
879
899
@@ -884,18 +904,16 @@ class _ComposeBoxContainer extends StatelessWidget {
884
904
885
905
@override
886
906
Widget build (BuildContext context) {
887
- ColorScheme colorScheme = Theme .of (context).colorScheme ;
907
+ final designVariables = DesignVariables .of (context);
888
908
889
909
// TODO(design): Maybe put a max width on the compose box, like we do on
890
910
// 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))),
892
914
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)));
899
917
}
900
918
}
901
919
@@ -916,45 +934,33 @@ class _ComposeBoxLayout extends StatelessWidget {
916
934
917
935
@override
918
936
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);
935
939
936
940
return _ComposeBoxContainer (
937
941
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
+ ])),
958
964
]));
959
965
}
960
966
}
0 commit comments