Skip to content

Commit 2be7ede

Browse files
committed
stream colors [nfc]: Move StreamColorSwatch out to a new file in widgets/
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
1 parent eaae4a6 commit 2be7ede

9 files changed

+604
-586
lines changed

lib/api/model/model.dart

Lines changed: 1 addition & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import 'package:flutter/foundation.dart';
2-
import 'package:flutter/painting.dart';
3-
import 'package:flutter_color_models/flutter_color_models.dart';
42
import 'package:json_annotation/json_annotation.dart';
53

6-
import '../../widgets/color.dart';
4+
import '../../widgets/stream_colors.dart';
75
import 'events.dart';
86
import 'initial_snapshot.dart';
97
import 'reaction.dart';
@@ -459,156 +457,6 @@ class Subscription extends ZulipStream {
459457
Map<String, dynamic> toJson() => _$SubscriptionToJson(this);
460458
}
461459

462-
/// A [ColorSwatch] with colors related to a base stream color.
463-
///
464-
/// Use this in UI code for colors related to [Subscription.color],
465-
/// such as the background of an unread count badge.
466-
class StreamColorSwatch extends ColorSwatch<StreamColorVariant> {
467-
StreamColorSwatch.light(int base) : this._(base, _computeLight(base));
468-
StreamColorSwatch.dark(int base) : this._(base, _computeDark(base));
469-
470-
const StreamColorSwatch._(int base, this._swatch) : super(base, _swatch);
471-
472-
final Map<StreamColorVariant, Color> _swatch;
473-
474-
/// The [Subscription.color] int that the swatch is based on.
475-
Color get base => this[StreamColorVariant.base]!;
476-
477-
Color get unreadCountBadgeBackground => this[StreamColorVariant.unreadCountBadgeBackground]!;
478-
479-
/// The stream icon on a plain-colored surface, such as white.
480-
///
481-
/// For the icon on a [barBackground]-colored surface,
482-
/// use [iconOnBarBackground] instead.
483-
Color get iconOnPlainBackground => this[StreamColorVariant.iconOnPlainBackground]!;
484-
485-
/// The stream icon on a [barBackground]-colored surface.
486-
///
487-
/// For the icon on a plain surface, use [iconOnPlainBackground] instead.
488-
/// This color is chosen to enhance contrast with [barBackground]:
489-
/// <https://github.com/zulip/zulip/pull/27485>
490-
Color get iconOnBarBackground => this[StreamColorVariant.iconOnBarBackground]!;
491-
492-
/// The background color of a bar representing a stream, like a recipient bar.
493-
///
494-
/// Use this in the message list, the "Inbox" view, and the "Streams" view.
495-
Color get barBackground => this[StreamColorVariant.barBackground]!;
496-
497-
static Map<StreamColorVariant, Color> _computeLight(int base) {
498-
final baseAsColor = Color(base);
499-
500-
final clamped20to75 = clampLchLightness(baseAsColor, 20, 75);
501-
final clamped20to75AsHsl = HSLColor.fromColor(clamped20to75);
502-
503-
return {
504-
StreamColorVariant.base: baseAsColor,
505-
506-
// Follows `.unread-count` in Vlad's replit:
507-
// <https://replit.com/@VladKorobov/zulip-sidebar#script.js>
508-
// <https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/design.3A.20.23F117.20.22Inbox.22.20screen/near/1624484>
509-
//
510-
// TODO fix bug where our results differ from the replit's (see unit tests)
511-
StreamColorVariant.unreadCountBadgeBackground:
512-
clampLchLightness(baseAsColor, 30, 70)
513-
.withOpacity(0.3),
514-
515-
// Follows `.sidebar-row__icon` in Vlad's replit:
516-
// <https://replit.com/@VladKorobov/zulip-sidebar#script.js>
517-
//
518-
// TODO fix bug where our results differ from the replit's (see unit tests)
519-
StreamColorVariant.iconOnPlainBackground: clamped20to75,
520-
521-
// Follows `.recepeient__icon` in Vlad's replit:
522-
// <https://replit.com/@VladKorobov/zulip-topic-feed-colors#script.js>
523-
// <https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/design.3A.20.23F117.20.22Inbox.22.20screen/near/1624484>
524-
//
525-
// TODO fix bug where our results differ from the replit's (see unit tests)
526-
StreamColorVariant.iconOnBarBackground:
527-
clamped20to75AsHsl
528-
.withLightness(clampDouble(clamped20to75AsHsl.lightness - 0.12, 0.0, 1.0))
529-
.toColor(),
530-
531-
// Follows `.recepient` in Vlad's replit:
532-
// <https://replit.com/@VladKorobov/zulip-topic-feed-colors#script.js>
533-
//
534-
// TODO I think [LabColor.interpolate] doesn't actually do LAB mixing;
535-
// it just calls up to the superclass method [ColorModel.interpolate]:
536-
// <https://pub.dev/documentation/flutter_color_models/latest/flutter_color_models/ColorModel/interpolate.html>
537-
// which does ordinary RGB mixing. Investigate and send a PR?
538-
// TODO fix bug where our results differ from the replit's (see unit tests)
539-
StreamColorVariant.barBackground:
540-
LabColor.fromColor(const Color(0xfff9f9f9))
541-
.interpolate(LabColor.fromColor(clamped20to75), 0.22)
542-
.toColor(),
543-
};
544-
}
545-
546-
static Map<StreamColorVariant, Color> _computeDark(int base) {
547-
final baseAsColor = Color(base);
548-
549-
final clamped20to75 = clampLchLightness(baseAsColor, 20, 75);
550-
551-
return {
552-
// See comments in [_computeLight] about what these computations are based
553-
// on, and how the resulting values are a little off sometimes. The
554-
// comments mostly apply here too.
555-
556-
StreamColorVariant.base: baseAsColor,
557-
StreamColorVariant.unreadCountBadgeBackground:
558-
clampLchLightness(baseAsColor, 30, 70)
559-
.withOpacity(0.3),
560-
StreamColorVariant.iconOnPlainBackground: clamped20to75,
561-
562-
// Follows the web app (as of zulip/zulip@db03369ac); see
563-
// get_stream_privacy_icon_color in web/src/stream_color.ts.
564-
//
565-
// `.recepeient__icon` in Vlad's replit gives something different so we
566-
// don't use that:
567-
// <https://replit.com/@VladKorobov/zulip-topic-feed-colors#script.js>
568-
// <https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/design.3A.20.23F117.20.22Inbox.22.20screen/near/1624484>
569-
// But that's OK because Vlad said "I feel like current dark theme contrast
570-
// is fine", and when he said that, this had been the web app's icon color
571-
// for 6+ months (since zulip/zulip@023584e04):
572-
// https://chat.zulip.org/#narrow/stream/101-design/topic/UI.20redesign.3A.20recipient.20bar.20colors/near/1675786
573-
//
574-
// TODO fix bug where our results are unexpected (see unit tests)
575-
StreamColorVariant.iconOnBarBackground: clamped20to75,
576-
577-
StreamColorVariant.barBackground:
578-
LabColor.fromColor(const Color(0xff000000))
579-
.interpolate(LabColor.fromColor(clamped20to75), 0.38)
580-
.toColor(),
581-
};
582-
}
583-
584-
/// Copied from [ColorSwatch.lerp].
585-
static StreamColorSwatch? lerp(StreamColorSwatch? a, StreamColorSwatch? b, double t) {
586-
if (identical(a, b)) {
587-
return a;
588-
}
589-
final Map<StreamColorVariant, Color> swatch;
590-
if (b == null) {
591-
swatch = a!._swatch.map((key, color) => MapEntry(key, Color.lerp(color, null, t)!));
592-
} else {
593-
if (a == null) {
594-
swatch = b._swatch.map((key, color) => MapEntry(key, Color.lerp(null, color, t)!));
595-
} else {
596-
swatch = a._swatch.map((key, color) => MapEntry(key, Color.lerp(color, b[key], t)!));
597-
}
598-
}
599-
return StreamColorSwatch._(Color.lerp(a, b, t)!.value, swatch);
600-
}
601-
}
602-
603-
@visibleForTesting
604-
enum StreamColorVariant {
605-
base,
606-
unreadCountBadgeBackground,
607-
iconOnPlainBackground,
608-
iconOnBarBackground,
609-
barBackground,
610-
}
611-
612460
@JsonEnum(fieldRename: FieldRename.snake, valueField: "apiValue")
613461
enum UserTopicVisibilityPolicy {
614462
none(apiValue: 0),

lib/widgets/stream_colors.dart

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import 'dart:ui';
2+
3+
import 'package:flutter/material.dart';
4+
import 'package:flutter_color_models/flutter_color_models.dart';
5+
6+
import '../api/model/model.dart';
7+
import 'color.dart';
8+
9+
/// A [ColorSwatch] with colors related to a base stream color.
10+
///
11+
/// Use this in UI code for colors related to [Subscription.color],
12+
/// such as the background of an unread count badge.
13+
class StreamColorSwatch extends ColorSwatch<StreamColorVariant> {
14+
StreamColorSwatch.light(int base) : this._(base, _computeLight(base));
15+
StreamColorSwatch.dark(int base) : this._(base, _computeDark(base));
16+
17+
const StreamColorSwatch._(int base, this._swatch) : super(base, _swatch);
18+
19+
final Map<StreamColorVariant, Color> _swatch;
20+
21+
/// The [Subscription.color] int that the swatch is based on.
22+
Color get base => this[StreamColorVariant.base]!;
23+
24+
Color get unreadCountBadgeBackground => this[StreamColorVariant.unreadCountBadgeBackground]!;
25+
26+
/// The stream icon on a plain-colored surface, such as white.
27+
///
28+
/// For the icon on a [barBackground]-colored surface,
29+
/// use [iconOnBarBackground] instead.
30+
Color get iconOnPlainBackground => this[StreamColorVariant.iconOnPlainBackground]!;
31+
32+
/// The stream icon on a [barBackground]-colored surface.
33+
///
34+
/// For the icon on a plain surface, use [iconOnPlainBackground] instead.
35+
/// This color is chosen to enhance contrast with [barBackground]:
36+
/// <https://github.com/zulip/zulip/pull/27485>
37+
Color get iconOnBarBackground => this[StreamColorVariant.iconOnBarBackground]!;
38+
39+
/// The background color of a bar representing a stream, like a recipient bar.
40+
///
41+
/// Use this in the message list, the "Inbox" view, and the "Streams" view.
42+
Color get barBackground => this[StreamColorVariant.barBackground]!;
43+
44+
static Map<StreamColorVariant, Color> _computeLight(int base) {
45+
final baseAsColor = Color(base);
46+
47+
final clamped20to75 = clampLchLightness(baseAsColor, 20, 75);
48+
final clamped20to75AsHsl = HSLColor.fromColor(clamped20to75);
49+
50+
return {
51+
StreamColorVariant.base: baseAsColor,
52+
53+
// Follows `.unread-count` in Vlad's replit:
54+
// <https://replit.com/@VladKorobov/zulip-sidebar#script.js>
55+
// <https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/design.3A.20.23F117.20.22Inbox.22.20screen/near/1624484>
56+
//
57+
// TODO fix bug where our results differ from the replit's (see unit tests)
58+
StreamColorVariant.unreadCountBadgeBackground:
59+
clampLchLightness(baseAsColor, 30, 70)
60+
.withOpacity(0.3),
61+
62+
// Follows `.sidebar-row__icon` in Vlad's replit:
63+
// <https://replit.com/@VladKorobov/zulip-sidebar#script.js>
64+
//
65+
// TODO fix bug where our results differ from the replit's (see unit tests)
66+
StreamColorVariant.iconOnPlainBackground: clamped20to75,
67+
68+
// Follows `.recepeient__icon` in Vlad's replit:
69+
// <https://replit.com/@VladKorobov/zulip-topic-feed-colors#script.js>
70+
// <https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/design.3A.20.23F117.20.22Inbox.22.20screen/near/1624484>
71+
//
72+
// TODO fix bug where our results differ from the replit's (see unit tests)
73+
StreamColorVariant.iconOnBarBackground:
74+
clamped20to75AsHsl
75+
.withLightness(clampDouble(clamped20to75AsHsl.lightness - 0.12, 0.0, 1.0))
76+
.toColor(),
77+
78+
// Follows `.recepient` in Vlad's replit:
79+
// <https://replit.com/@VladKorobov/zulip-topic-feed-colors#script.js>
80+
//
81+
// TODO I think [LabColor.interpolate] doesn't actually do LAB mixing;
82+
// it just calls up to the superclass method [ColorModel.interpolate]:
83+
// <https://pub.dev/documentation/flutter_color_models/latest/flutter_color_models/ColorModel/interpolate.html>
84+
// which does ordinary RGB mixing. Investigate and send a PR?
85+
// TODO fix bug where our results differ from the replit's (see unit tests)
86+
StreamColorVariant.barBackground:
87+
LabColor.fromColor(const Color(0xfff9f9f9))
88+
.interpolate(LabColor.fromColor(clamped20to75), 0.22)
89+
.toColor(),
90+
};
91+
}
92+
93+
static Map<StreamColorVariant, Color> _computeDark(int base) {
94+
final baseAsColor = Color(base);
95+
96+
final clamped20to75 = clampLchLightness(baseAsColor, 20, 75);
97+
98+
return {
99+
// See comments in [_computeLight] about what these computations are based
100+
// on, and how the resulting values are a little off sometimes. The
101+
// comments mostly apply here too.
102+
103+
StreamColorVariant.base: baseAsColor,
104+
StreamColorVariant.unreadCountBadgeBackground:
105+
clampLchLightness(baseAsColor, 30, 70)
106+
.withOpacity(0.3),
107+
StreamColorVariant.iconOnPlainBackground: clamped20to75,
108+
109+
// Follows the web app (as of zulip/zulip@db03369ac); see
110+
// get_stream_privacy_icon_color in web/src/stream_color.ts.
111+
//
112+
// `.recepeient__icon` in Vlad's replit gives something different so we
113+
// don't use that:
114+
// <https://replit.com/@VladKorobov/zulip-topic-feed-colors#script.js>
115+
// <https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/design.3A.20.23F117.20.22Inbox.22.20screen/near/1624484>
116+
// But that's OK because Vlad said "I feel like current dark theme contrast
117+
// is fine", and when he said that, this had been the web app's icon color
118+
// for 6+ months (since zulip/zulip@023584e04):
119+
// https://chat.zulip.org/#narrow/stream/101-design/topic/UI.20redesign.3A.20recipient.20bar.20colors/near/1675786
120+
//
121+
// TODO fix bug where our results are unexpected (see unit tests)
122+
StreamColorVariant.iconOnBarBackground: clamped20to75,
123+
124+
StreamColorVariant.barBackground:
125+
LabColor.fromColor(const Color(0xff000000))
126+
.interpolate(LabColor.fromColor(clamped20to75), 0.38)
127+
.toColor(),
128+
};
129+
}
130+
131+
/// Copied from [ColorSwatch.lerp].
132+
static StreamColorSwatch? lerp(StreamColorSwatch? a, StreamColorSwatch? b, double t) {
133+
if (identical(a, b)) {
134+
return a;
135+
}
136+
final Map<StreamColorVariant, Color> swatch;
137+
if (b == null) {
138+
swatch = a!._swatch.map((key, color) => MapEntry(key, Color.lerp(color, null, t)!));
139+
} else {
140+
if (a == null) {
141+
swatch = b._swatch.map((key, color) => MapEntry(key, Color.lerp(null, color, t)!));
142+
} else {
143+
swatch = a._swatch.map((key, color) => MapEntry(key, Color.lerp(color, b[key], t)!));
144+
}
145+
}
146+
return StreamColorSwatch._(Color.lerp(a, b, t)!.value, swatch);
147+
}
148+
}
149+
150+
@visibleForTesting
151+
enum StreamColorVariant {
152+
base,
153+
unreadCountBadgeBackground,
154+
iconOnPlainBackground,
155+
iconOnBarBackground,
156+
barBackground,
157+
}

lib/widgets/unread_count_badge.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
import 'package:flutter/material.dart';
33

4-
import '../api/model/model.dart';
4+
import 'stream_colors.dart';
55
import 'text.dart';
66

77
/// A widget to display a given number of unreads in a conversation.

test/api/model/model_checks.dart

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import 'dart:ui';
2-
31
import 'package:checks/checks.dart';
42
import 'package:zulip/api/model/model.dart';
53

@@ -29,14 +27,6 @@ extension ZulipStreamChecks on Subject<ZulipStream> {
2927
Subject<int?> get canRemoveSubscribersGroup => has((e) => e.canRemoveSubscribersGroup, 'canRemoveSubscribersGroup');
3028
}
3129

32-
extension StreamColorSwatchChecks on Subject<StreamColorSwatch> {
33-
Subject<Color> get base => has((s) => s.base, 'base');
34-
Subject<Color> get unreadCountBadgeBackground => has((s) => s.unreadCountBadgeBackground, 'unreadCountBadgeBackground');
35-
Subject<Color> get iconOnPlainBackground => has((s) => s.iconOnPlainBackground, 'iconOnPlainBackground');
36-
Subject<Color> get iconOnBarBackground => has((s) => s.iconOnBarBackground, 'iconOnBarBackground');
37-
Subject<Color> get barBackground => has((s) => s.barBackground, 'barBackground');
38-
}
39-
4030
extension MessageChecks on Subject<Message> {
4131
Subject<int> get id => has((e) => e.id, 'id');
4232
Subject<String> get content => has((e) => e.content, 'content');

0 commit comments

Comments
 (0)