@@ -14,13 +14,15 @@ import '../model/compose.dart';
1414import '../model/narrow.dart' ;
1515import '../model/store.dart' ;
1616import 'autocomplete.dart' ;
17+ import 'color.dart' ;
1718import 'dialog.dart' ;
1819import 'icons.dart' ;
20+ import 'inset_shadow.dart' ;
1921import 'store.dart' ;
22+ import 'text.dart' ;
2023import 'theme.dart' ;
2124
22- const double _inputVerticalPadding = 8 ;
23- const double _sendButtonSize = 36 ;
25+ const double _composeButtonSize = 44 ;
2426
2527/// A [TextEditingController] for use in the compose box.
2628///
@@ -364,34 +366,77 @@ class _ContentInputState extends State<_ContentInput> with WidgetsBindingObserve
364366 }
365367 }
366368
369+ static double maxHeight (BuildContext context) {
370+ final clampingTextScaler = MediaQuery .textScalerOf (context)
371+ .clamp (maxScaleFactor: 1.5 );
372+ final scaledLineHeight = clampingTextScaler.scale (_fontSize) * _lineHeightRatio;
373+
374+ // Reserve space to fully show the first 7th lines and just partially
375+ // clip the 8th line, where the height matches the spec at
376+ // https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=3960-5147&node-type=text&m=dev
377+ // > Maximum size of the compose box is suggested to be 178px. Which
378+ // > has 7 fully visible lines of text
379+ //
380+ // The partial line hints that the content input is scrollable.
381+ //
382+ // Using the ambient TextScale means this works for different values of the
383+ // system text-size setting. We clamp to a max scale factor to limit
384+ // how tall the content input can get; that's to save room for the message
385+ // list. The user can still scroll the input to see everything.
386+ return _verticalPadding + 7.727 * scaledLineHeight;
387+ }
388+
389+ static const _verticalPadding = 8.0 ;
390+ static const _fontSize = 17.0 ;
391+ static const _lineHeight = 22.0 ;
392+ static const _lineHeightRatio = _lineHeight / _fontSize;
393+
367394 @override
368395 Widget build (BuildContext context) {
369- ColorScheme colorScheme = Theme .of (context).colorScheme;
370-
371- return InputDecorator (
372- decoration: const InputDecoration (),
373- child: ConstrainedBox (
374- constraints: const BoxConstraints (
375- minHeight: _sendButtonSize - 2 * _inputVerticalPadding,
376-
377- // TODO constrain this adaptively (i.e. not hard-coded 200)
378- maxHeight: 200 ,
379- ),
380- child: ComposeAutocomplete (
381- narrow: widget.narrow,
382- controller: widget.controller,
383- focusNode: widget.focusNode,
384- fieldViewBuilder: (context) {
385- return TextField (
396+ final designVariables = DesignVariables .of (context);
397+
398+ return ComposeAutocomplete (
399+ narrow: widget.narrow,
400+ controller: widget.controller,
401+ focusNode: widget.focusNode,
402+ fieldViewBuilder: (context) => ConstrainedBox (
403+ constraints: BoxConstraints (maxHeight: maxHeight (context)),
404+ // This [ClipRect] replaces the [TextField] clipping we disable below.
405+ child: ClipRect (
406+ child: InsetShadowBox (
407+ top: _verticalPadding, bottom: _verticalPadding,
408+ color: designVariables.composeBoxBg,
409+ child: TextField (
386410 controller: widget.controller,
387411 focusNode: widget.focusNode,
388- style: TextStyle (color: colorScheme.onSurface),
389- decoration: InputDecoration .collapsed (hintText: widget.hintText),
412+ // Let the content show through the `contentPadding` so that
413+ // our [InsetShadowBox] can fade it smoothly there.
414+ clipBehavior: Clip .none,
415+ style: TextStyle (
416+ fontSize: _fontSize,
417+ height: _lineHeightRatio,
418+ color: designVariables.textInput),
419+ // From the spec at
420+ // https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=3960-5147&node-type=text&m=dev
421+ // > Compose box has the height to fit 2 lines. This is [done] to
422+ // > have a bigger hit area for the user to start the input. […]
423+ minLines: 2 ,
390424 maxLines: null ,
391425 textCapitalization: TextCapitalization .sentences,
392- );
393- }),
394- ));
426+ decoration: InputDecoration (
427+ // This padding ensures that the user can always scroll long
428+ // content entirely out of the top or bottom shadow if desired.
429+ // With this and the `minLines: 2` above, an empty content input
430+ // gets 60px vertical distance (with no text-size scaling)
431+ // between the top of the top shadow and the bottom of the
432+ // bottom shadow. That's a bit more than the 54px given in the
433+ // Figma, and we can revisit if needed, but it's tricky to get
434+ // that 54px distance while also making the scrolling work like
435+ // this and offering two lines of touchable area.
436+ contentPadding: const EdgeInsets .symmetric (vertical: _verticalPadding),
437+ hintText: widget.hintText,
438+ hintStyle: TextStyle (
439+ color: designVariables.textInput.withFadedAlpha (0.5 ))))))));
395440 }
396441}
397442
@@ -474,20 +519,32 @@ class _TopicInput extends StatelessWidget {
474519 @override
475520 Widget build (BuildContext context) {
476521 final zulipLocalizations = ZulipLocalizations .of (context);
477- ColorScheme colorScheme = Theme .of (context).colorScheme;
522+ final designVariables = DesignVariables .of (context);
523+ TextStyle topicTextStyle = TextStyle (
524+ fontSize: 20 ,
525+ height: 22 / 20 ,
526+ color: designVariables.textInput.withFadedAlpha (0.9 ),
527+ ).merge (weightVariableTextStyle (context, wght: 600 ));
478528
479529 return TopicAutocomplete (
480530 streamId: streamId,
481531 controller: controller,
482532 focusNode: focusNode,
483533 contentFocusNode: contentFocusNode,
484- fieldViewBuilder: (context) => TextField (
485- controller: controller,
486- focusNode: focusNode,
487- textInputAction: TextInputAction .next,
488- style: TextStyle (color: colorScheme.onSurface),
489- decoration: InputDecoration (hintText: zulipLocalizations.composeBoxTopicHintText),
490- ));
534+ fieldViewBuilder: (context) => Container (
535+ padding: const EdgeInsets .only (top: 10 , bottom: 9 ),
536+ decoration: BoxDecoration (border: Border (bottom: BorderSide (
537+ width: 1 ,
538+ color: designVariables.foreground.withFadedAlpha (0.2 )))),
539+ child: TextField (
540+ controller: controller,
541+ focusNode: focusNode,
542+ textInputAction: TextInputAction .next,
543+ style: topicTextStyle,
544+ decoration: InputDecoration (
545+ hintText: zulipLocalizations.composeBoxTopicHintText,
546+ hintStyle: topicTextStyle.copyWith (
547+ color: designVariables.textInput.withFadedAlpha (0.5 ))))));
491548 }
492549}
493550
@@ -660,12 +717,14 @@ abstract class _AttachUploadsButton extends StatelessWidget {
660717
661718 @override
662719 Widget build (BuildContext context) {
663- ColorScheme colorScheme = Theme .of (context).colorScheme ;
720+ final designVariables = DesignVariables .of (context);
664721 final zulipLocalizations = ZulipLocalizations .of (context);
665- return IconButton (
666- icon: Icon (icon, color: colorScheme.onSurfaceVariant),
667- tooltip: tooltip (zulipLocalizations),
668- onPressed: () => _handlePress (context));
722+ return SizedBox (
723+ width: _composeButtonSize,
724+ child: IconButton (
725+ icon: Icon (icon, color: designVariables.foreground.withFadedAlpha (0.5 )),
726+ tooltip: tooltip (zulipLocalizations),
727+ onPressed: () => _handlePress (context)));
669728 }
670729}
671730
@@ -929,38 +988,22 @@ class _SendButtonState extends State<_SendButton> {
929988
930989 @override
931990 Widget build (BuildContext context) {
932- final disabled = _hasValidationErrors;
933- final colorScheme = Theme .of (context).colorScheme;
991+ final designVariables = DesignVariables .of (context);
934992 final zulipLocalizations = ZulipLocalizations .of (context);
935993
936- // Copy FilledButton defaults (_FilledButtonDefaultsM3.backgroundColor)
937- final backgroundColor = disabled
938- ? colorScheme.onSurface.withValues (alpha: 0.12 )
939- : colorScheme.primary;
994+ final iconColor = _hasValidationErrors
995+ ? designVariables.icon.withFadedAlpha (0.5 )
996+ : designVariables.icon;
940997
941- // Copy FilledButton defaults (_FilledButtonDefaultsM3.foregroundColor)
942- final foregroundColor = disabled
943- ? colorScheme.onSurface.withValues (alpha: 0.38 )
944- : colorScheme.onPrimary;
945-
946- return Ink (
947- decoration: BoxDecoration (
948- borderRadius: const BorderRadius .all (Radius .circular (8.0 )),
949- color: backgroundColor,
950- ),
998+ return SizedBox (
999+ width: _composeButtonSize,
9511000 child: IconButton (
9521001 tooltip: zulipLocalizations.composeBoxSendTooltip,
953- style: const ButtonStyle (
954- // Match the height of the content input.
955- minimumSize: WidgetStatePropertyAll (Size .square (_sendButtonSize)),
956- // With the default of [MaterialTapTargetSize.padded], not just the
957- // tap target but the visual button would get padded to 48px square.
958- // It would be nice if the tap target extended invisibly out from the
959- // button, to make a 48px square, but that's not the behavior we get.
960- tapTargetSize: MaterialTapTargetSize .shrinkWrap,
961- ),
962- color: foregroundColor,
963- icon: const Icon (ZulipIcons .send),
1002+ icon: Icon (ZulipIcons .send,
1003+ // We set [Icon.color] instead of [IconButton.color] because the
1004+ // latter implicitly uses colors derived from it to override the
1005+ // ambient [ButtonStyle.overlayColor].
1006+ color: iconColor),
9641007 onPressed: _send));
9651008 }
9661009}
@@ -972,18 +1015,17 @@ class _ComposeBoxContainer extends StatelessWidget {
9721015
9731016 @override
9741017 Widget build (BuildContext context) {
975- ColorScheme colorScheme = Theme .of (context).colorScheme ;
1018+ final designVariables = DesignVariables .of (context);
9761019
9771020 // TODO(design): Maybe put a max width on the compose box, like we do on
9781021 // the message list itself
979- return SizedBox (width: double .infinity,
1022+ return Container (width: double .infinity,
1023+ decoration: BoxDecoration (
1024+ border: Border (top: BorderSide (color: designVariables.borderBar))),
9801025 child: Material (
981- color: colorScheme.surfaceContainerHighest,
982- child: SafeArea (
983- minimum: const EdgeInsets .fromLTRB (8 , 0 , 8 , 8 ),
984- child: Padding (
985- padding: const EdgeInsets .only (top: 8.0 ),
986- child: child))));
1026+ color: designVariables.composeBoxBg,
1027+ child: SafeArea (minimum: const EdgeInsets .symmetric (horizontal: 8 ),
1028+ child: child)));
9871029 }
9881030}
9891031
@@ -1004,22 +1046,14 @@ class _ComposeBoxLayout extends StatelessWidget {
10041046
10051047 @override
10061048 Widget build (BuildContext context) {
1007- ThemeData themeData = Theme .of (context);
1008- ColorScheme colorScheme = themeData.colorScheme;
1049+ final themeData = Theme .of (context);
10091050
10101051 final inputThemeData = themeData.copyWith (
1011- inputDecorationTheme: InputDecorationTheme (
1052+ inputDecorationTheme: const InputDecorationTheme (
10121053 // Both [contentPadding] and [isDense] combine to make the layout compact.
10131054 isDense: true ,
1014- contentPadding: const EdgeInsets .symmetric (
1015- horizontal: 12.0 , vertical: _inputVerticalPadding),
1016- border: const OutlineInputBorder (
1017- borderRadius: BorderRadius .all (Radius .circular (4.0 )),
1018- borderSide: BorderSide .none),
1019- filled: true ,
1020- fillColor: colorScheme.surface,
1021- ),
1022- );
1055+ contentPadding: EdgeInsets .zero,
1056+ border: InputBorder .none));
10231057
10241058 final composeButtons = [
10251059 _AttachFileButton (contentController: contentController, contentFocusNode: contentFocusNode),
@@ -1029,19 +1063,22 @@ class _ComposeBoxLayout extends StatelessWidget {
10291063
10301064 return _ComposeBoxContainer (
10311065 child: Column (children: [
1032- Row (crossAxisAlignment: CrossAxisAlignment .end, children: [
1033- Expanded (
1034- child: Theme (
1035- data: inputThemeData,
1036- child: Column (children: [
1037- if (topicInput != null ) topicInput! ,
1038- if (topicInput != null ) const SizedBox (height: 8 ),
1039- contentInput,
1040- ]))),
1041- const SizedBox (width: 8 ),
1042- sendButton,
1043- ]),
1044- Row (children: composeButtons),
1066+ Padding (
1067+ padding: const EdgeInsets .symmetric (horizontal: 8 ),
1068+ child: Theme (
1069+ data: inputThemeData,
1070+ child: Column (children: [
1071+ if (topicInput != null ) topicInput! ,
1072+ contentInput,
1073+ ]))),
1074+ SizedBox (
1075+ height: _composeButtonSize,
1076+ child: Row (
1077+ mainAxisAlignment: MainAxisAlignment .spaceBetween,
1078+ children: [
1079+ Row (children: composeButtons),
1080+ sendButton,
1081+ ])),
10451082 ]));
10461083 }
10471084}
0 commit comments