-
Notifications
You must be signed in to change notification settings - Fork 255
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
ui: Provide [StreamColorSwatch]es via DesignVariables, not api/model/ #746
ui: Provide [StreamColorSwatch]es via DesignVariables, not api/model/ #746
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Glad to get this moved out of lib/api/
. Comments below.
test/widgets/inbox_test.dart
Outdated
testWidgets('uncollapsed header changes background color when [subscription.color] changes', (tester) async { | ||
final initialColor = Colors.indigo.value; | ||
|
||
await setupVarious(tester, sub1Color: initialColor); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of setupVarious
, how about just setting up the one subscription this uses and one message in that stream?
That'd make this test more self-contained — easier to see how the things it's doing relate to each other, and less potential for interference with the needs of other tests that use the same all-in-one miscellaneous setup.
lib/widgets/stream_colors.dart
Outdated
import '../api/model/model.dart'; | ||
import 'color.dart'; | ||
|
||
/// A caching factory for [StreamColorSwatch]es, from [subscription.color] bases. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what a "caching factory" is. Can you try explaining this without the word "factory"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You give it a [subscription.color]
, and it returns a [StreamColorSwatch]
for it. If there's one cached, it returns that, otherwise it creates one, caches it, and returns it. This is done through the forBaseColor
method.
(I guess "creates one" is just what I was trying to get at with "factory".)
lib/widgets/stream_colors.dart
Outdated
// No public constructor; always use [instance]. Empirically, | ||
// [StreamColorSwatches.lerp] is called even when the theme has not changed, | ||
// and the short-circuit on [identical] cuts redundant work when that happens. | ||
static StreamColorSwatchesLight get instance => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about calling this StreamColorSwatches.light
, and making this class private?
It can still work the same way, I'm imagining purely a rename. But I think that will look a bit cleaner where it's used than if the consuming code has the various different subclasses to think about.
lib/widgets/stream_colors.dart
Outdated
static StreamColorSwatchesLight get instance => | ||
(_instance ??= StreamColorSwatchesLight._()); | ||
static StreamColorSwatchesLight? _instance; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Separately, this can be simplified to:
static StreamColorSwatchesLight get instance => | |
(_instance ??= StreamColorSwatchesLight._()); | |
static StreamColorSwatchesLight? _instance; | |
static final StreamColorSwatchesLight instance = StreamColorSwatchesLight._(); |
which is equivalent. (A static field is initialized lazily when first accessed.)
lib/widgets/stream_colors.dart
Outdated
@override | ||
StreamColorSwatch forBaseColor(int base) => | ||
_cache[base] ??= StreamColorSwatch.light(base); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the caching logic works the same in all three subclasses, it can be brought up to the base class. Then this can look like:
@override | |
StreamColorSwatch forBaseColor(int base) => | |
_cache[base] ??= StreamColorSwatch.light(base); | |
@override | |
StreamColorSwatch computeForBaseColor(int base) => StreamColorSwatch.light(base); |
(This is something the Flutter framework does, for things like RenderBox.computeMinIntrinsicWidth
.)
test/widgets/stream_colors_test.dart
Outdated
test('from dark to light', () { | ||
final instance = StreamColorSwatchesDark.instance | ||
.lerp(StreamColorSwatchesLight.instance, 0.4); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this test is redundant with the one above it — there isn't any logic in the lerping that works differently between dark and light, so these are just two distinct groups of swatches whichever way around they are.
test/widgets/theme_test.dart
Outdated
|
||
group('colorSwatchFor', () { | ||
void doTest(int baseColor, Brightness brightness) { | ||
final description = '${Color(baseColor).toString()}, $brightness'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
final description = '${Color(baseColor).toString()}, $brightness'; | |
final description = '${Color(baseColor)}, $brightness'; |
test/widgets/theme_test.dart
Outdated
final expectedSwatch = switch (brightness) { | ||
Brightness.light => StreamColorSwatch.light(baseColor), | ||
Brightness.dark => StreamColorSwatch.dark(baseColor), | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you want to pass an expected
parameter instead? I'm not sure how this switch (brightness)
will handle the lerping case in the TODO below. (And the dark
case is relevant only as part of implementing that TODO.)
acb9654
to
e81f84e
Compare
Thanks for the review! Revision pushed. |
There was a problem hiding this 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! Small comments, largely nits.
lib/widgets/stream_colors.dart
Outdated
/// A provider for [StreamColorSwatch]es that computes and caches them. | ||
abstract class StreamColorSwatches { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think "provider", like "factory", is a word I'd like to avoid here in favor of unpacking what it means 🙂
Working from your explanation at #746 (comment) , here's a version I think would be clear:
/// A provider for [StreamColorSwatch]es that computes and caches them. | |
abstract class StreamColorSwatches { | |
/// Computes a [StreamColorSwatch] for any given color, with caching. | |
abstract class StreamColorSwatches { |
Perhaps better yet, here's a slightly different way of looking at it:
/// A provider for [StreamColorSwatch]es that computes and caches them. | |
abstract class StreamColorSwatches { | |
/// A lazily-computed map from a stream's base color to a | |
/// corresponding [StreamColorSwatch]. | |
abstract class StreamColorSwatches { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, yeah; I like the second one.
/// This computation is cached on the instance | ||
/// in order to save work building [t]'s animation frame when there are | ||
/// multiple UI elements using the same [subscription.color]. | ||
StreamColorSwatches lerp(StreamColorSwatches? other, double t) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps make this static? That seems to be the convention elsewhere, or at least on Color
.
(I had this thought when looking at the top of the class declaration, where there's light
and dark
and what one basically wants to say is that we have three kinds of instances of this class: light, dark, and lerps between them.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I can try that. I think I did non-static because that's what ThemeExtension
does. (CodeBlockTextStyles
came out non-static for that reason as well, I think.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TextStyle
's lerp is also static.
…Hmm, although I guess maybe CodeBlockTextStyles
and StreamColorSwatches
are more like ThemeExtension
than like Color
and TextStyle
?
One thing about CodeBlockTextStyles
and StreamColorSwatches
is that their lerps are only ever called by a ThemeExtension
's lerp. In that lerp, the a
instance (i.e., this
) is non-nullable, and that's convenient for _StreamColorSwatchesLerped.computeForBaseColor
because it makes it easy to reason locally that its non-nullable StreamColorSwatch
return type is correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I did non-static because that's what
ThemeExtension
does.
Hmm I see.
I just did a quick survey of the Flutter tree. It looks like the pattern is very consistent, and ThemeExtension
is an outlier. So let's follow the typical pattern and make it static.
Specifically the search I did was, in the Flutter repo:
$ git grep ' lerp[<(]' packages/flutter/lib/
That finds definitions of a method named lerp
, static or otherwise.
There are a number of non-static lerp
methods… on Tween
subclasses and the like, that look like this:
packages/flutter/lib/src/rendering/tweens.dart
27: FractionalOffset? lerp(double t) => FractionalOffset.lerp(begin, end, t);
50: Alignment lerp(double t) => Alignment.lerp(begin, end, t)!;
75: AlignmentGeometry? lerp(double t) => AlignmentGeometry.lerp(begin, end, t);
So e.g. AlignmentTween.lerp
is non-static; but it's not a lerp between AlignmentTween
s, rather a lerp between Alignment
s, and in fact it forwards to the static Alignment.lerp
.
There are something like 90 static lerp
methods that take both endpoints as arguments, and like 40 non-static lerp
methods that take neither as arguments, getting them both from the receiver (as with those Tween subclasses). And I think ThemeExtension
is the sole example of its pattern, where it gets one endpoint from the receiver and the other as an argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(This also means that that API choice on ThemeExtension
was basically a mistake.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for that investigation!
lib/widgets/stream_colors.dart
Outdated
/// Gives the cached [StreamColorSwatch] for a [subscription.color]. | ||
StreamColorSwatch forBaseColor(int base) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// Gives the cached [StreamColorSwatch] for a [subscription.color]. | |
StreamColorSwatch forBaseColor(int base) => | |
/// Gives the [StreamColorSwatch] for a [subscription.color]. | |
StreamColorSwatch forBaseColor(int base) => |
I wouldn't say this gives "the cached swatch", because there may not have been a cached swatch yet when it was called.
lib/widgets/stream_colors.dart
Outdated
StreamColorSwatch forBaseColor(int base) => | ||
_cache[base] ??= computeForBaseColor(base); | ||
|
||
StreamColorSwatch computeForBaseColor(int base); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could also make this private, as nothing should be calling it directly other than the implementation of forBaseColor
.
lib/widgets/stream_colors.dart
Outdated
|
||
/// The [StreamColorSwatches] for the light theme. | ||
class _StreamColorSwatchesLight extends StreamColorSwatches { | ||
_StreamColorSwatchesLight._(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
_StreamColorSwatchesLight._(); | |
_StreamColorSwatchesLight(); |
Has the same effect since the class is already private, and makes the syntax a bit cleaner at the call site.
lib/widgets/stream_colors.dart
Outdated
/// The [StreamColorSwatches] for the light theme. | ||
class _StreamColorSwatchesLight extends StreamColorSwatches { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: move this doc to the static instance, which is public
e81f84e
to
48f282c
Compare
Thanks for the review! Revision pushed, and I replied above: #746 (comment) |
Thanks! All LGTM except that one open thread above. I'll go ahead and merge this, since that's something straightforward to fix up separately. Then as a followup let's fix both this |
I believe these stopped applying in ebff35f, and they're confusing; remove them.
I started this by writing streamHeaderBackgroundColor just for an upcoming test (checking the stream header responds to update-stream-color events). But it was a nice opportunity to complete some existing TODOs.
This will help verify the upcoming refactor, zulip#393 (storing stream-color variants in DesignVariables instead of Subscription model objects).
We'll want to get these colors from theme data, soon, for zulip#95 (supporting dark theme).
Just like we did for the InboxPage tests, in the previous commit. Here, there's one test that failed on the assumption that a particular icon was the only one in view; it apparently isn't anymore (probably because of the page's back button), and that assumption isn't important to the goal of the test. So, we adapt by removing that assumption. Related: zulip#393
Instead of putting them in Subscription model objects. This helps toward zulip#95 by bundling this with our other ThemeExtensions that will switch together between light and dark variants when the theme changes. It's also just nicer to separate this very UI-focused code out of api/model/ (zulip#393). Not marking [nfc] just because of differences in caching logic, although I don't expect a user-visible perf effect. Related: zulip#95 Fixes: zulip#393
48f282c
to
fba909b
Compare
Cool; sure, I'll follow up with a fix to make them static. |
(Instead of putting them in Subscription model objects.)
I wonder if there's a better approach than this. One thing is that having separate subclasses
StreamColorSwatchesLight
andStreamColorSwatchesDark
means some duplicated boilerplate.Related: #95
Fixes: #393