fix(app): a sheet's outlines and scrollbar stay clear of its edges
Found on the laptop: the outlines at a sheet's left (a text field, the round buttons) were shaved by half a pixel, and the scrollbar lay over the fields at the right. The sheet's room at its sides was around its scroll view, so the body's outlines lay on the scroll view's edge: a scroll view cuts what it holds there, an outline's outer, antialiased half pixel included, which shows wherever the edge falls between two pixels, as with a dialog centred in a window of odd width; and its scrollbar runs along that edge. The room now lies inside the scroll view, and the title and the footer keep their own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
2deabcb5b0
commit
c9faaf99c6
2 changed files with 58 additions and 9 deletions
|
|
@ -140,24 +140,40 @@ class const SheetScaffold({
|
|||
final presentation = _Presentation.at(context);
|
||||
final isDialog = presentation.isDialog;
|
||||
final heightFactor = this.heightFactor;
|
||||
// The sides' room goes inside the scroll view, not around it: a scroll
|
||||
// view cuts what it holds at its edges, outlines' outer half pixel
|
||||
// included, and its scrollbar runs along its edge.
|
||||
final side = isDialog ? 24.0 : 20.0;
|
||||
Widget inset(Widget child) => Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: side),
|
||||
child: child,
|
||||
);
|
||||
final column = Column(
|
||||
mainAxisSize: isDialog || heightFactor == null
|
||||
? MainAxisSize.min
|
||||
: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_Header(title: title, action: action, onClose: onClose),
|
||||
inset(_Header(title: title, action: action, onClose: onClose)),
|
||||
// Loose, so a short body keeps the sheet short; tight at a fixed
|
||||
// height, so the footer sits at the bottom.
|
||||
if (!isDialog && heightFactor != null)
|
||||
Expanded(
|
||||
child: _ScrollingBody(body: body, fill: true, footer: footer),
|
||||
child: _ScrollingBody(
|
||||
body: body,
|
||||
side: side,
|
||||
fill: true,
|
||||
footer: footer,
|
||||
),
|
||||
)
|
||||
else
|
||||
Flexible(
|
||||
child: _ScrollingBody(body: body, footer: footer),
|
||||
child: _ScrollingBody(body: body, side: side, footer: footer),
|
||||
),
|
||||
if (footer case final footer?) ...[const SizedBox(height: 12), footer],
|
||||
if (footer case final footer?) ...[
|
||||
const SizedBox(height: 12),
|
||||
inset(footer),
|
||||
],
|
||||
],
|
||||
);
|
||||
final content = hasSnackBars
|
||||
|
|
@ -179,16 +195,16 @@ class const SheetScaffold({
|
|||
maxHeight: math.min(dialogMaxHeight, media.size.height - 64),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 20, 24, 20),
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
child: content,
|
||||
),
|
||||
);
|
||||
}
|
||||
// A drag handle above gives the top its space.
|
||||
final padding = EdgeInsets.fromLTRB(
|
||||
20,
|
||||
0,
|
||||
presentation.hasDragHandle ? 0 : 16,
|
||||
20,
|
||||
0,
|
||||
16,
|
||||
);
|
||||
// What's left once the keyboard and the system's bottom inset are
|
||||
|
|
@ -261,6 +277,9 @@ class const _Header({
|
|||
class const _ScrollingBody({
|
||||
required final Widget body,
|
||||
required final Widget? footer,
|
||||
|
||||
/// The room at its sides, inside the scroll view.
|
||||
required final double side,
|
||||
final bool fill = false,
|
||||
}) extends StatefulWidget {
|
||||
@override
|
||||
|
|
@ -300,8 +319,14 @@ class _ScrollingBodyState() extends State<_ScrollingBody> {
|
|||
_ => false,
|
||||
};
|
||||
// The lines keep their room while hidden, so the body doesn't jump.
|
||||
Widget line({required bool shown, required Key key}) =>
|
||||
shown ? Divider(key: key, height: 1) : const SizedBox(height: 1);
|
||||
Widget line({required bool shown, required Key key}) => shown
|
||||
? Divider(
|
||||
key: key,
|
||||
height: 1,
|
||||
indent: widget.side,
|
||||
endIndent: widget.side,
|
||||
)
|
||||
: const SizedBox(height: 1);
|
||||
final scrolling = NotificationListener<ScrollMetricsNotification>(
|
||||
// Also when the body's size changes, as a section opens.
|
||||
onNotification: (notification) {
|
||||
|
|
@ -324,6 +349,7 @@ class _ScrollingBodyState() extends State<_ScrollingBody> {
|
|||
).copyWith(scrollbars: false),
|
||||
child: SingleChildScrollView(
|
||||
controller: _scroll,
|
||||
padding: EdgeInsets.symmetric(horizontal: widget.side),
|
||||
child: widget.body,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -167,4 +167,27 @@ void main() {
|
|||
expect(find.byKey(SheetScaffold.moreBelowKey), findsNothing);
|
||||
expect(find.byKey(SheetScaffold.moreAboveKey), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets("the body keeps clear of the scroll view's edges, where it is "
|
||||
'cut and the scrollbar runs', (tester) async {
|
||||
for (final width in [400.0, 1000.0]) {
|
||||
await open(
|
||||
tester,
|
||||
width: width,
|
||||
body: const TextField(
|
||||
decoration: InputDecoration(border: OutlineInputBorder()),
|
||||
),
|
||||
);
|
||||
|
||||
final scrolling = tester.getRect(find.byType(SingleChildScrollView));
|
||||
final field = tester.getRect(find.byType(TextField));
|
||||
expect(field.left - scrolling.left, greaterThanOrEqualTo(16));
|
||||
expect(scrolling.right - field.right, greaterThanOrEqualTo(16));
|
||||
// The title and the button stay in line with the body.
|
||||
expect(tester.getTopLeft(find.text('New studyset')).dx, field.left);
|
||||
expect(tester.getTopRight(find.byType(FilledButton)).dx, field.right);
|
||||
await tester.tap(find.byTooltip('Close'));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue