Skip to content

Commit

Permalink
FIX LINK WHEN MERGING compose_box: Support the redesigned layout for …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
PIG208 committed Sep 7, 2024
1 parent 82d07ce commit 6ec4857
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 114 deletions.
211 changes: 102 additions & 109 deletions lib/widgets/compose_box.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ import 'autocomplete.dart';
import 'dialog.dart';
import 'icons.dart';
import 'store.dart';
import 'text.dart';
import 'theme.dart';

const double _inputVerticalPadding = 8;
const double _sendButtonSize = 36;
const double _composeButtonWidth = 44;
const double _composeButtonHeight = 42;

/// A [TextEditingController] for use in the compose box.
///
Expand Down Expand Up @@ -285,32 +286,33 @@ class _ContentInput extends StatelessWidget {

@override
Widget build(BuildContext context) {
ColorScheme colorScheme = Theme.of(context).colorScheme;

return InputDecorator(
decoration: const InputDecoration(),
child: ConstrainedBox(
constraints: const BoxConstraints(
minHeight: _sendButtonSize - 2 * _inputVerticalPadding,

// TODO constrain this adaptively (i.e. not hard-coded 200)
maxHeight: 200,
),
child: ComposeAutocomplete(
narrow: narrow,
final designVariables = DesignVariables.of(context);
const topPadding = 8.0;
const contentLineHeight = 22.0;

return ConstrainedBox(
constraints: const BoxConstraints(
// Reserve space to fully show the first 7th lines and just partially
// clip the 8th line, where the height matches the spec of 178 logical
// pixels. The partial line hints that the content input is scrollable.
maxHeight: topPadding + contentLineHeight * 7 + contentLineHeight * 0.727),
child: ComposeAutocomplete(
narrow: narrow,
controller: controller,
focusNode: focusNode,
fieldViewBuilder: (context) => TextField(
controller: controller,
focusNode: focusNode,
fieldViewBuilder: (context) {
return TextField(
controller: controller,
focusNode: focusNode,
style: TextStyle(color: colorScheme.onSurface),
decoration: InputDecoration.collapsed(hintText: hintText),
maxLines: null,
textCapitalization: TextCapitalization.sentences,
);
}),
));
decoration: InputDecoration.collapsed(
hintText: hintText,
hintStyle: TextStyle(color: designVariables.textInput.withOpacity(0.5))),
minLines: 2,
maxLines: null,
textCapitalization: TextCapitalization.sentences,
style: TextStyle(
fontSize: 17,
height: (contentLineHeight / 17),
color: designVariables.textInput))));
}
}

Expand Down Expand Up @@ -391,20 +393,42 @@ class _TopicInput extends StatelessWidget {

@override
Widget build(BuildContext context) {
const textFieldHeight = 42;
const lineHeight = 22;
final zulipLocalizations = ZulipLocalizations.of(context);
ColorScheme colorScheme = Theme.of(context).colorScheme;
final designVariables = DesignVariables.of(context);
TextStyle topicTextStyle = TextStyle(
fontSize: 22,
height: lineHeight / 22,
color: designVariables.textInput,
).merge(weightVariableTextStyle(context, wght: 600));

return TopicAutocomplete(
streamId: streamId,
controller: controller,
focusNode: focusNode,
contentFocusNode: contentFocusNode,
fieldViewBuilder: (context) => TextField(
controller: controller,
focusNode: focusNode,
textInputAction: TextInputAction.next,
style: TextStyle(color: colorScheme.onSurface),
decoration: InputDecoration(hintText: zulipLocalizations.composeBoxTopicHintText),
fieldViewBuilder: (context) => Stack(
children: [
TextField(
controller: controller,
focusNode: focusNode,
textInputAction: TextInputAction.next,
style: topicTextStyle,
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(
vertical: (textFieldHeight - lineHeight) / 2),
border: InputBorder.none,
hintText: zulipLocalizations.composeBoxTopicHintText,
hintStyle: topicTextStyle.copyWith(
color: designVariables.textInput.withOpacity(0.5)))),
Positioned(bottom: 0, left: 0, right: 0,
child: Container(height: 1, decoration: BoxDecoration(
border: Border(
bottom: BorderSide(width: 1,
color: designVariables.foreground.withOpacity(0.2)))))),
],
));
}
}
Expand Down Expand Up @@ -578,10 +602,13 @@ abstract class _AttachUploadsButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final zulipLocalizations = ZulipLocalizations.of(context);
return IconButton(
icon: Icon(icon),
tooltip: tooltip(zulipLocalizations),
onPressed: () => _handlePress(context));
return SizedBox(
width: _composeButtonWidth,
child: IconButton(
icon: Icon(icon),
tooltip: tooltip(zulipLocalizations),
onPressed: () => _handlePress(context),
style: const ButtonStyle(splashFactory: NoSplash.splashFactory)));
}
}

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

@override
Widget build(BuildContext context) {
final disabled = _hasValidationErrors;
final colorScheme = Theme.of(context).colorScheme;
final designVariables = DesignVariables.of(context);
final zulipLocalizations = ZulipLocalizations.of(context);

// Copy FilledButton defaults (_FilledButtonDefaultsM3.backgroundColor)
final backgroundColor = disabled
? colorScheme.onSurface.withValues(alpha: 0.12)
: colorScheme.primary;

// Copy FilledButton defaults (_FilledButtonDefaultsM3.foregroundColor)
final foregroundColor = disabled
? colorScheme.onSurface.withValues(alpha: 0.38)
: colorScheme.onPrimary;

return Ink(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
color: backgroundColor,
),
return SizedBox(
width: _composeButtonWidth,
child: IconButton(
tooltip: zulipLocalizations.composeBoxSendTooltip,
style: const ButtonStyle(
// Match the height of the content input.
minimumSize: WidgetStatePropertyAll(Size.square(_sendButtonSize)),
// With the default of [MaterialTapTargetSize.padded], not just the
// tap target but the visual button would get padded to 48px square.
// It would be nice if the tap target extended invisibly out from the
// button, to make a 48px square, but that's not the behavior we get.
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
color: foregroundColor,
color: _hasValidationErrors
// TODO(design): need send button color when disabled
? designVariables.icon.withOpacity(0.5)
: designVariables.icon,
icon: const Icon(ZulipIcons.send),
onPressed: _send));
onPressed: _send,
style: const ButtonStyle(splashFactory: NoSplash.splashFactory)));
}
}

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

@override
Widget build(BuildContext context) {
ColorScheme colorScheme = Theme.of(context).colorScheme;
final designVariables = DesignVariables.of(context);

// TODO(design): Maybe put a max width on the compose box, like we do on
// the message list itself
return SizedBox(width: double.infinity,
return Container(width: double.infinity,
decoration: BoxDecoration(
border: Border(top: BorderSide(color: designVariables.borderBar))),
child: Material(
color: colorScheme.surfaceContainerHighest,
child: SafeArea(
minimum: const EdgeInsets.fromLTRB(8, 0, 8, 8),
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: child))));
color: designVariables.bgComposeBox,
child: SafeArea(child: child)));
}
}

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

@override
Widget build(BuildContext context) {
ThemeData themeData = Theme.of(context);
ColorScheme colorScheme = themeData.colorScheme;

final inputThemeData = themeData.copyWith(
inputDecorationTheme: InputDecorationTheme(
// Both [contentPadding] and [isDense] combine to make the layout compact.
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12.0, vertical: _inputVerticalPadding),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(4.0)),
borderSide: BorderSide.none),
filled: true,
fillColor: colorScheme.surface,
),
);
final themeData = Theme.of(context);
final designVariables = DesignVariables.of(context);

return _ComposeBoxContainer(
child: Column(children: [
Row(crossAxisAlignment: CrossAxisAlignment.end, children: [
Expanded(
child: Theme(
data: inputThemeData,
child: Column(children: [
if (topicInput != null) topicInput!,
if (topicInput != null) const SizedBox(height: 8),
contentInput,
]))),
const SizedBox(width: 8),
sendButton,
]),
Theme(
data: themeData.copyWith(
iconTheme: themeData.iconTheme.copyWith(color: colorScheme.onSurface.withOpacity(0.5))),
child: Row(children: [
_AttachFileButton(contentController: contentController, contentFocusNode: contentFocusNode),
_AttachMediaButton(contentController: contentController, contentFocusNode: contentFocusNode),
_AttachFromCameraButton(contentController: contentController, contentFocusNode: contentFocusNode),
])),
if (topicInput != null)
Padding(padding: const EdgeInsets.symmetric(horizontal: 16),
child: topicInput!),
Padding(padding: const EdgeInsets.symmetric(horizontal: 16),
child: contentInput),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
height: _composeButtonHeight,
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Theme(
data: themeData.copyWith(
iconTheme: themeData.iconTheme.copyWith(
color: designVariables.foreground.withOpacity(0.5))),
child: Row(children: [
_AttachFileButton(contentController: contentController, contentFocusNode: contentFocusNode),
_AttachMediaButton(contentController: contentController, contentFocusNode: contentFocusNode),
_AttachFromCameraButton(contentController: contentController, contentFocusNode: contentFocusNode),
])),
sendButton,
])),
]));
}
}
Expand Down
25 changes: 23 additions & 2 deletions lib/widgets/theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,14 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
bgCounterUnread: const Color(0xff666699).withValues(alpha: 0.15),
bgTopBar: const Color(0xfff5f5f5),
borderBar: const Color(0x33000000),
icon: const Color(0xff666699),
icon: const Color(0xff6159e1),
labelCounterUnread: const Color(0xff222222),
labelMenuButton: const Color(0xff222222),
mainBackground: const Color(0xfff0f0f0),
title: const Color(0xff1a1a1a),
bgComposeBox: const Color(0xffffffff),
textInput: const Color(0xff000000),
foreground: const Color(0xff000000),
channelColorSwatches: ChannelColorSwatches.light,
atMentionMarker: const HSLColor.fromAHSL(0.5, 0, 0, 0.2).toColor(),
dmHeaderBg: const HSLColor.fromAHSL(1, 46, 0.35, 0.93).toColor(),
Expand All @@ -138,11 +141,14 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
bgCounterUnread: const Color(0xff666699).withValues(alpha: 0.37),
bgTopBar: const Color(0xff242424),
borderBar: Colors.black.withValues(alpha: 0.41),
icon: const Color(0xff7070c2),
icon: const Color(0xff7977fe),
labelCounterUnread: const Color(0xffffffff).withValues(alpha: 0.7),
labelMenuButton: const Color(0xffffffff).withValues(alpha: 0.85),
mainBackground: const Color(0xff1d1d1d),
title: const Color(0xffffffff),
bgComposeBox: const Color(0xff0f0f0f),
textInput: const Color(0xffffffff).withValues(alpha: 0.9),
foreground: const Color(0xffffffff),
channelColorSwatches: ChannelColorSwatches.dark,
// TODO(design-dark) need proper dark-theme color (this is ad hoc)
atMentionMarker: const HSLColor.fromAHSL(0.4, 0, 0, 1).toColor(),
Expand Down Expand Up @@ -177,6 +183,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
required this.labelMenuButton,
required this.mainBackground,
required this.title,
required this.bgComposeBox,
required this.textInput,
required this.foreground,
required this.channelColorSwatches,
required this.atMentionMarker,
required this.dmHeaderBg,
Expand Down Expand Up @@ -213,6 +222,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
final Color labelMenuButton;
final Color mainBackground;
final Color title;
final Color bgComposeBox;
final Color textInput;
final Color foreground;

// Not exactly from the Figma design, but from Vlad anyway.
final ChannelColorSwatches channelColorSwatches;
Expand Down Expand Up @@ -244,6 +256,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
Color? labelMenuButton,
Color? mainBackground,
Color? title,
Color? bgComposeBox,
Color? textInput,
Color? foreground,
ChannelColorSwatches? channelColorSwatches,
Color? atMentionMarker,
Color? dmHeaderBg,
Expand All @@ -270,6 +285,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
labelMenuButton: labelMenuButton ?? this.labelMenuButton,
mainBackground: mainBackground ?? this.mainBackground,
title: title ?? this.title,
bgComposeBox: bgComposeBox ?? this.bgComposeBox,
textInput: textInput ?? this.textInput,
foreground: foreground ?? this.foreground,
channelColorSwatches: channelColorSwatches ?? this.channelColorSwatches,
atMentionMarker: atMentionMarker ?? this.atMentionMarker,
dmHeaderBg: dmHeaderBg ?? this.dmHeaderBg,
Expand Down Expand Up @@ -303,6 +321,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
labelMenuButton: Color.lerp(labelMenuButton, other.labelMenuButton, t)!,
mainBackground: Color.lerp(mainBackground, other.mainBackground, t)!,
title: Color.lerp(title, other.title, t)!,
bgComposeBox: Color.lerp(bgComposeBox, other.bgComposeBox, t)!,
textInput: Color.lerp(textInput, other.textInput, t)!,
foreground: Color.lerp(foreground, other.foreground, t)!,
channelColorSwatches: ChannelColorSwatches.lerp(channelColorSwatches, other.channelColorSwatches, t),
atMentionMarker: Color.lerp(atMentionMarker, other.atMentionMarker, t)!,
dmHeaderBg: Color.lerp(dmHeaderBg, other.dmHeaderBg, t)!,
Expand Down
7 changes: 4 additions & 3 deletions test/widgets/compose_box_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import 'package:zulip/model/narrow.dart';
import 'package:zulip/model/store.dart';
import 'package:zulip/widgets/compose_box.dart';
import 'package:zulip/widgets/icons.dart';
import 'package:zulip/widgets/theme.dart';

import '../api/fake_api.dart';
import '../example_data.dart' as eg;
Expand Down Expand Up @@ -255,10 +256,10 @@ void main() {
of: find.byIcon(ZulipIcons.send),
matching: find.byType(IconButton)));
final sendButtonWidget = sendButtonElement.widget as IconButton;
final colorScheme = Theme.of(sendButtonElement).colorScheme;
final designVariables = DesignVariables.of(sendButtonElement);
final expectedForegroundColor = expected
? colorScheme.onSurface.withValues(alpha: 0.38)
: colorScheme.onPrimary;
? designVariables.icon.withValues(alpha: 0.5)
: designVariables.icon;
check(sendButtonWidget.color).equals(expectedForegroundColor);
}

Expand Down

0 comments on commit 6ec4857

Please sign in to comment.