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

ui: Provide [StreamColorSwatch]es via DesignVariables, not api/model/ #746

Merged

Conversation

chrisbobbe
Copy link
Collaborator

(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 and StreamColorSwatchesDark means some duplicated boilerplate.

Related: #95
Fixes: #393

@chrisbobbe chrisbobbe added a-model Implementing our data model (PerAccountStore, etc.) integration review Added by maintainers when PR may be ready for integration labels Jun 18, 2024
@chrisbobbe chrisbobbe requested a review from gnprice June 18, 2024 19:56
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! Glad to get this moved out of lib/api/. Comments below.

testWidgets('uncollapsed header changes background color when [subscription.color] changes', (tester) async {
final initialColor = Colors.indigo.value;

await setupVarious(tester, sub1Color: initialColor);
Copy link
Member

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.

import '../api/model/model.dart';
import 'color.dart';

/// A caching factory for [StreamColorSwatch]es, from [subscription.color] bases.
Copy link
Member

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"?

Copy link
Collaborator Author

@chrisbobbe chrisbobbe Jun 20, 2024

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".)

// 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 =>
Copy link
Member

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.

Comment on lines 47 to 49
static StreamColorSwatchesLight get instance =>
(_instance ??= StreamColorSwatchesLight._());
static StreamColorSwatchesLight? _instance;
Copy link
Member

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:

Suggested change
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.)

Comment on lines 60 to 62
@override
StreamColorSwatch forBaseColor(int base) =>
_cache[base] ??= StreamColorSwatch.light(base);
Copy link
Member

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:

Suggested change
@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.)

Comment on lines 70 to 72
test('from dark to light', () {
final instance = StreamColorSwatchesDark.instance
.lerp(StreamColorSwatchesLight.instance, 0.4);
Copy link
Member

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.


group('colorSwatchFor', () {
void doTest(int baseColor, Brightness brightness) {
final description = '${Color(baseColor).toString()}, $brightness';
Copy link
Member

Choose a reason for hiding this comment

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

nit:

Suggested change
final description = '${Color(baseColor).toString()}, $brightness';
final description = '${Color(baseColor)}, $brightness';

Comment on lines 101 to 104
final expectedSwatch = switch (brightness) {
Brightness.light => StreamColorSwatch.light(baseColor),
Brightness.dark => StreamColorSwatch.dark(baseColor),
};
Copy link
Member

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.)

@chrisbobbe chrisbobbe force-pushed the pr-stream-color-swatches-design-variables branch from acb9654 to e81f84e Compare June 20, 2024 22:45
@chrisbobbe
Copy link
Collaborator Author

Thanks for the review! Revision pushed.

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! Small comments, largely nits.

Comment on lines 9 to 11
/// A provider for [StreamColorSwatch]es that computes and caches them.
abstract class StreamColorSwatches {
Copy link
Member

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:

Suggested change
/// 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:

Suggested change
/// 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 {

Copy link
Collaborator Author

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) {
Copy link
Member

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.)

Copy link
Collaborator Author

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.)

Copy link
Collaborator Author

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.

Copy link
Member

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 AlignmentTweens, rather a lerp between Alignments, 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.

Copy link
Member

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.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for that investigation!

Comment on lines 16 to 21
/// Gives the cached [StreamColorSwatch] for a [subscription.color].
StreamColorSwatch forBaseColor(int base) =>
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// 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.

StreamColorSwatch forBaseColor(int base) =>
_cache[base] ??= computeForBaseColor(base);

StreamColorSwatch computeForBaseColor(int base);
Copy link
Member

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.


/// The [StreamColorSwatches] for the light theme.
class _StreamColorSwatchesLight extends StreamColorSwatches {
_StreamColorSwatchesLight._();
Copy link
Member

Choose a reason for hiding this comment

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

nit:

Suggested change
_StreamColorSwatchesLight._();
_StreamColorSwatchesLight();

Has the same effect since the class is already private, and makes the syntax a bit cleaner at the call site.

Comment on lines 46 to 50
/// The [StreamColorSwatches] for the light theme.
class _StreamColorSwatchesLight extends StreamColorSwatches {
Copy link
Member

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

@chrisbobbe chrisbobbe force-pushed the pr-stream-color-swatches-design-variables branch from e81f84e to 48f282c Compare June 21, 2024 19:22
@chrisbobbe
Copy link
Collaborator Author

Thanks for the review! Revision pushed, and I replied above: #746 (comment)

@gnprice
Copy link
Member

gnprice commented Jun 21, 2024

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 lerp method and the existing other lerp methods to be static, as discussed in that thread #746 (comment) .

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).
This should help the tests be more representative generally; but in
particular, ZulipApp is going to start providing the stream color
swatches, instead of code in api/model, for zulip#393. So, we have the
test setup use ZulipApp so that we don't get a crash in InboxPage.

Related: zulip#393
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
…ets/

Toward zulip#393, "model: Track/memoize stream-color variants somewhere
other than /api/model".

But this only moves definitions; we'll reassign the
caching/computing logic in an upcoming commit.

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
@gnprice gnprice force-pushed the pr-stream-color-swatches-design-variables branch from 48f282c to fba909b Compare June 21, 2024 22:32
@gnprice gnprice merged commit fba909b into zulip:main Jun 21, 2024
1 check passed
@chrisbobbe chrisbobbe deleted the pr-stream-color-swatches-design-variables branch June 21, 2024 22:43
@chrisbobbe
Copy link
Collaborator Author

Cool; sure, I'll follow up with a fix to make them static.

chrisbobbe added a commit to chrisbobbe/zulip-flutter that referenced this pull request Jun 21, 2024
chrisbobbe added a commit to chrisbobbe/zulip-flutter that referenced this pull request Jun 21, 2024
chrisbobbe added a commit to chrisbobbe/zulip-flutter that referenced this pull request Jun 26, 2024
chrisbobbe added a commit to chrisbobbe/zulip-flutter that referenced this pull request Jun 26, 2024
chrisbobbe added a commit to chrisbobbe/zulip-flutter that referenced this pull request Jun 26, 2024
chrisbobbe added a commit to chrisbobbe/zulip-flutter that referenced this pull request Jun 26, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a-model Implementing our data model (PerAccountStore, etc.) 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.

model: Track/memoize stream-color variants somewhere other than /api/model
2 participants