@@ -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