Skip to content

model: Implement StreamColorSwatch.lerp, copying ColorSwatch.lerp #654

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

Merged
merged 3 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 44 additions & 14 deletions lib/api/model/model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -462,61 +462,72 @@ class Subscription extends ZulipStream {
///
/// Use this in UI code for colors related to [Subscription.color],
/// such as the background of an unread count badge.
class StreamColorSwatch extends ColorSwatch<_StreamColorVariant> {
StreamColorSwatch(int base) : super(base, _compute(base));
class StreamColorSwatch extends ColorSwatch<StreamColorVariant> {
StreamColorSwatch(int base) : this._(base, _compute(base));

Color get base => this[_StreamColorVariant.base]!;
const StreamColorSwatch._(int base, this._swatch) : super(base, _swatch);

Color get unreadCountBadgeBackground => this[_StreamColorVariant.unreadCountBadgeBackground]!;
/// A [StreamColorSwatch], from a [Map<_StreamColorVariant, Color>]
/// written manually.
@visibleForTesting
const StreamColorSwatch.debugFromBaseAndSwatch(int base, swatch)
: this._(base, swatch);

final Map<StreamColorVariant, Color> _swatch;

/// The [Subscription.color] int that the swatch is based on.
Color get base => this[StreamColorVariant.base]!;

Color get unreadCountBadgeBackground => this[StreamColorVariant.unreadCountBadgeBackground]!;

/// The stream icon on a plain-colored surface, such as white.
///
/// For the icon on a [barBackground]-colored surface,
/// use [iconOnBarBackground] instead.
Color get iconOnPlainBackground => this[_StreamColorVariant.iconOnPlainBackground]!;
Color get iconOnPlainBackground => this[StreamColorVariant.iconOnPlainBackground]!;

/// The stream icon on a [barBackground]-colored surface.
///
/// For the icon on a plain surface, use [iconOnPlainBackground] instead.
/// This color is chosen to enhance contrast with [barBackground]:
/// <https://github.com/zulip/zulip/pull/27485>
Color get iconOnBarBackground => this[_StreamColorVariant.iconOnBarBackground]!;
Color get iconOnBarBackground => this[StreamColorVariant.iconOnBarBackground]!;

/// The background color of a bar representing a stream, like a recipient bar.
///
/// Use this in the message list, the "Inbox" view, and the "Streams" view.
Color get barBackground => this[_StreamColorVariant.barBackground]!;
Color get barBackground => this[StreamColorVariant.barBackground]!;

static Map<_StreamColorVariant, Color> _compute(int base) {
static Map<StreamColorVariant, Color> _compute(int base) {
final baseAsColor = Color(base);

final clamped20to75 = clampLchLightness(baseAsColor, 20, 75);
final clamped20to75AsHsl = HSLColor.fromColor(clamped20to75);

return {
_StreamColorVariant.base: baseAsColor,
StreamColorVariant.base: baseAsColor,

// Follows `.unread-count` in Vlad's replit:
// <https://replit.com/@VladKorobov/zulip-sidebar#script.js>
// <https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/design.3A.20.23F117.20.22Inbox.22.20screen/near/1624484>
//
// TODO fix bug where our results differ from the replit's (see unit tests)
_StreamColorVariant.unreadCountBadgeBackground:
StreamColorVariant.unreadCountBadgeBackground:
clampLchLightness(baseAsColor, 30, 70)
.withOpacity(0.3),

// Follows `.sidebar-row__icon` in Vlad's replit:
// <https://replit.com/@VladKorobov/zulip-topic-feed-colors#script.js>
//
// TODO fix bug where our results differ from the replit's (see unit tests)
_StreamColorVariant.iconOnPlainBackground: clamped20to75,
StreamColorVariant.iconOnPlainBackground: clamped20to75,

// Follows `.recepeient__icon` in Vlad's replit:
// <https://replit.com/@VladKorobov/zulip-topic-feed-colors#script.js>
// <https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/design.3A.20.23F117.20.22Inbox.22.20screen/near/1624484>
//
// TODO fix bug where our results differ from the replit's (see unit tests)
_StreamColorVariant.iconOnBarBackground:
StreamColorVariant.iconOnBarBackground:
clamped20to75AsHsl
.withLightness(clamped20to75AsHsl.lightness - 0.12)
.toColor(),
Expand All @@ -529,15 +540,34 @@ class StreamColorSwatch extends ColorSwatch<_StreamColorVariant> {
// <https://pub.dev/documentation/flutter_color_models/latest/flutter_color_models/ColorModel/interpolate.html>
// which does ordinary RGB mixing. Investigate and send a PR?
// TODO fix bug where our results differ from the replit's (see unit tests)
_StreamColorVariant.barBackground:
StreamColorVariant.barBackground:
LabColor.fromColor(const Color(0xfff9f9f9))
.interpolate(LabColor.fromColor(clamped20to75), 0.22)
.toColor(),
};
}

/// Copied from [ColorSwatch.lerp].
static StreamColorSwatch? lerp(StreamColorSwatch? a, StreamColorSwatch? b, double t) {
if (identical(a, b)) {
return a;
}
final Map<StreamColorVariant, Color> swatch;
if (b == null) {
swatch = a!._swatch.map((key, color) => MapEntry(key, Color.lerp(color, null, t)!));
} else {
if (a == null) {
swatch = b._swatch.map((key, color) => MapEntry(key, Color.lerp(null, color, t)!));
} else {
swatch = a._swatch.map((key, color) => MapEntry(key, Color.lerp(color, b[key], t)!));
}
}
return StreamColorSwatch._(Color.lerp(a, b, t)!.value, swatch);
}
}

enum _StreamColorVariant {
@visibleForTesting
enum StreamColorVariant {
base,
unreadCountBadgeBackground,
iconOnPlainBackground,
Expand Down
42 changes: 42 additions & 0 deletions test/api/model/model_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,48 @@ void main() {
runCheck(0xffa47462, const Color(0xffe7dad6));
runCheck(0xffacc25d, const Color(0xffe9edd6));
});

test('lerp (different a, b)', () {
final swatchA = StreamColorSwatch(0xff76ce90);

// TODO(#95) use something like StreamColorSwatch.dark(), once
// implemented, and remove debugFromBaseAndSwatch
const swatchB = StreamColorSwatch.debugFromBaseAndSwatch(0xff76ce90, <StreamColorVariant, Color>{
StreamColorVariant.base: Color(0xff76ce90),
StreamColorVariant.unreadCountBadgeBackground: Color(0x4d65bd80),
StreamColorVariant.iconOnPlainBackground: Color(0xff73cb8d),
StreamColorVariant.iconOnBarBackground: Color(0xff73cb8d),
StreamColorVariant.barBackground: Color(0xff2e4935),
});

for (final t in [0.0, 0.5, 1.0, -0.1, 1.1]) {
final result = StreamColorSwatch.lerp(swatchA, swatchB, t)!;
for (final variant in StreamColorVariant.values) {
final (subject, expected) = switch (variant) {
StreamColorVariant.base => (check(result).base,
Color.lerp(swatchA.base, swatchB.base, t)!),
StreamColorVariant.unreadCountBadgeBackground => (check(result).unreadCountBadgeBackground,
Color.lerp(swatchA.unreadCountBadgeBackground, swatchB.unreadCountBadgeBackground, t)!),
StreamColorVariant.iconOnPlainBackground => (check(result).iconOnPlainBackground,
Color.lerp(swatchA.iconOnPlainBackground, swatchB.iconOnPlainBackground, t)!),
StreamColorVariant.iconOnBarBackground => (check(result).iconOnBarBackground,
Color.lerp(swatchA.iconOnBarBackground, swatchB.iconOnBarBackground, t)!),
StreamColorVariant.barBackground => (check(result).barBackground,
Color.lerp(swatchA.barBackground, swatchB.barBackground, t)!),
};
subject.equals(expected);
}
}
});

test('lerp (identical a, b)', () {
check(StreamColorSwatch.lerp(null, null, 0.0)).isNull();

final swatch = StreamColorSwatch(0xff76ce90);
check(StreamColorSwatch.lerp(swatch, swatch, 0.5)).isNotNull()
..identicalTo(swatch)
..base.equals(const Color(0xff76ce90));
});
});
});

Expand Down