Skip to content

Commit c6fd05c

Browse files
committed
compose_box: Implement content input redesign
`ClipRect`'s size is determined by the `ConstrainedBox`. This is mainly for showing the content through the `contentPadding` of the `TextField`, so that our `InsetShadowBox` can fade it smoothly there. The shadow is always there, but it is only visible when the `TextField` is long enough to be scrollable. See also: - zulip#928 (comment), which elaborates on the issue we intend to solve with the `ClipRect` setup. - https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=3954-13395 - https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=3862-14350 Fixes: zulip#915 Signed-off-by: Zixuan James Li <[email protected]>
1 parent 527aeb5 commit c6fd05c

File tree

1 file changed

+65
-20
lines changed

1 file changed

+65
-20
lines changed

lib/widgets/compose_box.dart

+65-20
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import 'autocomplete.dart';
1717
import 'color.dart';
1818
import 'dialog.dart';
1919
import 'icons.dart';
20+
import 'inset_shadow.dart';
2021
import 'store.dart';
2122
import 'text.dart';
2223
import 'theme.dart';
@@ -366,32 +367,76 @@ class _ContentInputState extends State<_ContentInput> with WidgetsBindingObserve
366367
}
367368
}
368369

370+
static double maxHeight(BuildContext context) {
371+
final clampingTextScaler = MediaQuery.textScalerOf(context)
372+
.clamp(maxScaleFactor: 1.5);
373+
final scaledLineHeight = clampingTextScaler.scale(fontSize) * lineHeightRatio;
374+
375+
// Reserve space to fully show the first 7th lines and just partially
376+
// clip the 8th line, where the height matches the spec at
377+
// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=3960-5147&node-type=text&m=dev
378+
// > Maximum size of the compose box is suggested to be 178px. Which
379+
// > has 7 fully visible lines of text
380+
//
381+
// The partial line hints that the content input is scrollable.
382+
//
383+
// Using the ambient TextScale means this works for different values of the
384+
// system text-size setting. We clamp to a max scale factor to limit
385+
// how tall the content input can get; that's to save room for the message
386+
// list. The user can still scroll the input to see everything.
387+
return verticalPadding + 7.727 * scaledLineHeight;
388+
}
389+
390+
static const verticalPadding = 8.0;
391+
static const fontSize = 17.0;
392+
static const lineHeight = 22.0;
393+
static const lineHeightRatio = lineHeight / fontSize;
394+
369395
@override
370396
Widget build(BuildContext context) {
371-
ColorScheme colorScheme = Theme.of(context).colorScheme;
372-
373-
return InputDecorator(
374-
decoration: const InputDecoration(),
375-
child: ConstrainedBox(
376-
constraints: const BoxConstraints(
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(
397+
final designVariables = DesignVariables.of(context);
398+
399+
return ComposeAutocomplete(
400+
narrow: widget.narrow,
401+
controller: widget.controller,
402+
focusNode: widget.focusNode,
403+
fieldViewBuilder: (context) => ConstrainedBox(
404+
constraints: BoxConstraints(maxHeight: maxHeight(context)),
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 between the top of the top shadow
431+
// and the bottom of the bottom shadow (with no text-size
432+
// scaling). That's a bit more than the 54px given in the Figma,
433+
// and we can revisit if needed, but it's tricky to get that
434+
// 54px distance while also making the scrolling work like this
435+
// 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

0 commit comments

Comments
 (0)