Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

compose_box [nfc]: Add InsetShadowBox #990

Merged
merged 1 commit into from
Oct 11, 2024
Merged

Conversation

PIG208
Copy link
Member

@PIG208 PIG208 commented Oct 8, 2024

This casts a static shadow on top of a child widget from its top edge and bottom edge.

To review its effect, see #928. This is intended to prep for #853.

@PIG208 PIG208 added the maintainer review PR ready for review by Zulip maintainers label Oct 8, 2024
@PIG208 PIG208 requested a review from chrisbobbe October 8, 2024 00:54
Copy link
Collaborator

@chrisbobbe chrisbobbe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Small comments below, and I'll mark for Greg's review.

@@ -0,0 +1,45 @@
import 'package:flutter/material.dart';

/// A widget that overlays inset shadows on a child.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you expand on this a little bit, maybe mentioning the scroll-through-shadow use case?

There's a similar-sounding effect that this looks like it might describe, but it turns out it doesn't; that's box-shadow: inset. It would be nice to have a full implementation of box-shadow: inset—we have a TODO in the reaction-chip UI:

      // TODO shadow effect, following web, which uses `box-shadow: inset`:
      //   https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow#inset
      //   Needs Flutter support for something like that:
      //     https://github.com/flutter/flutter/issues/18636
      //     https://github.com/flutter/flutter/issues/52999
      //   Until then use a solid color; a much-lightened version of the shadow color.
      //   Also adapt by making [borderUnselected] more transparent, so we'll
      //   want to check that against web when implementing the shadow.
      bgUnselected: const HSLColor.fromAHSL(0.08, 210, 0.50, 0.875).toColor(),

—but that's not what InsetShadowBox is. 🙂 (InsetShadowBox wouldn't handle the "stadium" shape of the reaction chips, for example.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. I think this means that we should rename this widget to RectangularInsetShadowBox, to make it clearer.

Comment on lines 35 to 50
Widget build(BuildContext context) {
return Stack(
children: [
child,
Positioned(top: 0, left: 0, right: 0,
child: Container(height: top, decoration: _shadowFrom(Alignment.topCenter))),
Positioned(bottom: 0, left: 0, right: 0,
child: Container(height: bottom, decoration: _shadowFrom(Alignment.bottomCenter))),
]);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Widget build(BuildContext context) {
return Stack(
children: [
child,
Positioned(top: 0, left: 0, right: 0,
child: Container(height: top, decoration: _shadowFrom(Alignment.topCenter))),
Positioned(bottom: 0, left: 0, right: 0,
child: Container(height: bottom, decoration: _shadowFrom(Alignment.bottomCenter))),
]);
}
Widget build(BuildContext context) {
return Stack(children: [
child,
Positioned(top: 0, left: 0, right: 0,
child: Container(height: top, decoration: _shadowFrom(Alignment.topCenter))),
Positioned(bottom: 0, left: 0, right: 0,
child: Container(height: bottom, decoration: _shadowFrom(Alignment.bottomCenter))),
]);
}

@@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';

(not sure how much we care about being agnostic about Material though.)

@chrisbobbe chrisbobbe added integration review Added by maintainers when PR may be ready for integration and removed maintainer review PR ready for review by Zulip maintainers labels Oct 8, 2024
@chrisbobbe chrisbobbe assigned gnprice and unassigned chrisbobbe Oct 8, 2024
@PIG208 PIG208 force-pushed the pr-inset-shadow branch 3 times, most recently from 978c9be to 8fd9107 Compare October 8, 2024 19:41
Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @PIG208, and thanks @chrisbobbe for the previous review!

Generally this looks great. A few nits below.

Let's also have a test or two.

  • One thing to test is that the shadows don't add padding — so e.g. a child with height 40 and shadows of height 8 produce height 40, not 56.
  • Ideally I'd like to have a test for the widget's actual main functionality too. I'm not sure there's a good way to do that without more work than it's worth, though.
    • One way would be with golden images. That seems like definitely more work than it's worth here.
    • The other possible way would be a painting test. That'd be worth spending like 15 minutes investigating to try to get to work — it might not be hard.

Comment on lines 45 to 46
Positioned(top: 0, left: 0, right: 0,
child: Container(height: top, decoration: _shadowFrom(Alignment.topCenter))),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Positioned can control the height, right?

Suggested change
Positioned(top: 0, left: 0, right: 0,
child: Container(height: top, decoration: _shadowFrom(Alignment.topCenter))),
Positioned(top: 0, height: top, left: 0, right: 0,
child: Container(decoration: _shadowFrom(Alignment.topCenter))),

Then also Container can I think be reduced to DecoratedBox.

///
/// See also:
/// * https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=3860-11890&node-type=frame&t=oOVTdwGZgtvKv9i8-0
class RectangularInsetShadowBox extends StatelessWidget {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"rectangular box" seems redundant — a box is necessarily rectangular. I think InsetShadowBox would be a fine name.

Or I guess this is a box with rectangular inset shadows (the shadows are rectangular). But I think an "inset shadow" on a box, which is a rectangle, is also going to be rectangular.

It's true this doesn't cover as wide a range of cases as CSS box-shadow: inset, as discussed at #990 (comment) . I think that reflects the CSS feature growing to be wider than its name fits. To clarify that this doesn't cover that CSS feature, the clearest thing would be to just mention it in the doc.

@@ -0,0 +1,51 @@
import 'package:flutter/widgets.dart';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: generally we've followed the Flutter upstream tree in grouping related functionality together into source files, rather than atomizing each class into its own tiny file.

One consequence is that the file names are shorter and more general than the class names.

Here there might not be any existing other code for this to share a file with, but we can still give it a bit of a broader, shorter name. Say inset_shadow.dart.

@PIG208
Copy link
Member Author

PIG208 commented Oct 9, 2024

Updated the PR with tests and fixes. This is ready for review!

@PIG208 PIG208 requested a review from gnprice October 9, 2024 00:50
Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the revision! Comments below.


import '../flutter_checks.dart';

void main() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test doesn't actually run in flutter test (or tools/check) because the name doesn't end with _test.dart 🙂

return (Symbol methodName, List<dynamic> arguments) {
check(methodName).equals(#drawRect);
check(arguments[0]).isA<Rect>().equals(rect);
check(arguments[1]).isA<Paint>().shader.isA<ui.Gradient>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth a one-line comment saying that Gradient is opaque and so this is the most we can check about it.

Comment on lines 18 to 24
child: SizedBox(width: 20, height: 20,
child: InsetShadowBox(top: 7, bottom: 3,
color: Colors.red,
child: Placeholder())))));

final childRect = tester.getRect(find.byType(Placeholder));
check(childRect).equals(const Rect.fromLTRB(0, 0, 20, 20));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this check would pass no matter what InsetShadowBox attempted to do for its layout. Its parent (the SizedBox) has decreed exactly what its size is, so it's not able to be any other size. See:
https://docs.flutter.dev/get-started/fundamentals/layout#constraints
https://api.flutter.dev/flutter/rendering/RenderBox-class.html#layout

It's worth trying to verify that — edit InsetShadowBox so that it does use top and bottom as padding, and see if this test still passes. I believe it will.

To make the test effective, I think what's needed is to have a SizedBox as the child, not the parent. Then do confirm that that test fails if you make that change to InsetShadowBox.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We actually caught a bug with the implementation that allows InsetShadowBox to modify the constraints from the parent. The test is updated to catch layout changes.

child: SizedBox(width: 20, height: 20,
child: InsetShadowBox(top: 7, bottom: 3,
color: Colors.red,
child: Placeholder())))));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In tests, typically preferable to say something like SizedBox.shrink() instead of Placeholder. The Placeholder widget has a more complicated behavior that's useful for making it visible in development, but in a test makes the behavior more complex to reason through.

This casts fixed size shadows on top of a child widget
from its top edge and bottom edge.

Signed-off-by: Zixuan James Li <[email protected]>
@gnprice
Copy link
Member

gnprice commented Oct 11, 2024

Thanks for the revision! Looks good; merging.

@gnprice gnprice merged commit a1b5579 into zulip:main Oct 11, 2024
@PIG208 PIG208 deleted the pr-inset-shadow branch October 11, 2024 00:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
integration review Added by maintainers when PR may be ready for integration
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants