From 867c28f83b0e5b161dcbd9d16201b290e400be7a Mon Sep 17 00:00:00 2001 From: Chris Bobbe Date: Fri, 3 May 2024 15:02:12 -0700 Subject: [PATCH] model: Implement `streamColorSwatchDark`, for dark mode We added the light-mode color computations in #381. We're not ready to use this yet, but the color computations are unlikely to change before that time comes, and finding the right ones is a chore that's good to get done. Related: #95 --- lib/api/model/model.dart | 63 +++++++++++ test/api/model/model_test.dart | 196 +++++++++++++++++++++++++++++++++ 2 files changed, 259 insertions(+) diff --git a/lib/api/model/model.dart b/lib/api/model/model.dart index 1b4aef45ce..4f253c44ca 100644 --- a/lib/api/model/model.dart +++ b/lib/api/model/model.dart @@ -463,6 +463,8 @@ class Subscription extends ZulipStream { /// /// Use this in UI code for colors related to [Subscription.color], /// such as the background of an unread count badge. +/// +/// For the dark-mode colors, see [streamColorSwatchDark]. ColorSwatch streamColorSwatch(int base) { final baseAsColor = Color(base); @@ -514,6 +516,67 @@ ColorSwatch streamColorSwatch(int base) { return ColorSwatch(base, map); } +/// A [ColorSwatch] with dark-mode colors related to a base stream color. +/// +/// Use this in UI code for colors related to [Subscription.color], +/// such as the background of an unread count badge. +/// +/// For the light-mode colors, see [streamColorSwatch]. +ColorSwatch streamColorSwatchDark(int base) { + final baseAsColor = Color(base); + + final clamped20to75 = clampLchLightness(baseAsColor, 20, 75); + + final map = { + StreamColor.base: baseAsColor, + + // Follows `.unread-count` in Vlad's replit: + // + // + // + // TODO fix bug where our results differ from the replit's (see unit tests) + StreamColor.unreadCountBadgeBackground: + clampLchLightness(baseAsColor, 30, 70) + .withOpacity(0.3), + + // Follows `.sidebar-row__icon` in Vlad's replit: + // + // + // TODO fix bug where our results differ from the replit's (see unit tests) + StreamColor.iconOnPlainBackground: clamped20to75, + + // Follows the web app (as of zulip/zulip@db03369ac); see + // get_stream_privacy_icon_color in web/src/stream_color.ts. + // + // `.recepeient__icon` in Vlad's replit gives something different so we + // don't use that: + // + // + // But that's OK because Vlad said "I feel like current dark theme contrast + // is fine", and when he said that, this had been the web app's icon color + // for 6+ months (since zulip/zulip@023584e04): + // https://chat.zulip.org/#narrow/stream/101-design/topic/UI.20redesign.3A.20recipient.20bar.20colors/near/1675786 + // + // TODO fix bug where our results are unexpected (see unit tests) + StreamColor.iconOnBarBackground: clamped20to75, + + // Follows `.recepient` in Vlad's replit: + // + // + // TODO I think [LabColor.interpolate] doesn't actually do LAB mixing; + // it just calls up to the superclass method [ColorModel.interpolate]: + // + // which does ordinary RGB mixing. Investigate and send a PR? + // TODO fix bug where our results differ from the replit's (see unit tests) + StreamColor.barBackground: + LabColor.fromColor(const Color(0xff000000)) + .interpolate(LabColor.fromColor(clamped20to75), 0.38) + .toColor(), + }; + + return ColorSwatch(base, map); +} + enum StreamColor { /// The [Subscription.color] int that the swatch is based on. base, diff --git a/test/api/model/model_test.dart b/test/api/model/model_test.dart index afa8a4b541..1ab41ece86 100644 --- a/test/api/model/model_test.dart +++ b/test/api/model/model_test.dart @@ -325,6 +325,202 @@ void main() { runCheck(0xffacc25d, const Color(0xffe9edd6)); }); }); + + group('streamColorSwatchDark', () { + test('base', () { + check(streamColorSwatchDark(0xffffffff))[StreamColor.base] + .equals(const Color(0xffffffff)); + }); + + test('unreadCountBadgeBackground', () { + void runCheck(int base, Color expected) { + check(streamColorSwatchDark(base)) + [StreamColor.unreadCountBadgeBackground].equals(expected); + } + + // Check against everything in ZULIP_ASSIGNMENT_COLORS and EXTREME_COLORS + // in . + // On how to extract expected results from the replit, see: + // https://github.com/zulip/zulip-flutter/pull/643#issuecomment-2093940972 + + // TODO Fix bug causing our implementation's results to differ from the + // replit's. Where they differ, see comment with what the replit gives. + + // ZULIP_ASSIGNMENT_COLORS + runCheck(0xff76ce90, const Color(0x4d65bd80)); + runCheck(0xfffae589, const Color(0x4dbdab53)); // 0x4dbdaa52 + runCheck(0xffa6c7e5, const Color(0x4d8eafcc)); // 0x4d8fb0cd + runCheck(0xffe79ab5, const Color(0x4de295b0)); // 0x4de194af + runCheck(0xffbfd56f, const Color(0x4d9eb551)); // 0x4d9eb450 + runCheck(0xfff4ae55, const Color(0x4de19d45)); // 0x4de09c44 + runCheck(0xffb0a5fd, const Color(0x4daba0f8)); // 0x4daca2f9 + runCheck(0xffaddfe5, const Color(0x4d83b4b9)); // 0x4d83b4ba + runCheck(0xfff5ce6e, const Color(0x4dcba749)); // 0x4dcaa648 + runCheck(0xffc2726a, const Color(0x4dc2726a)); + runCheck(0xff94c849, const Color(0x4d86ba3c)); // 0x4d86ba3b + runCheck(0xffbd86e5, const Color(0x4dbd86e5)); + runCheck(0xffee7e4a, const Color(0x4dee7e4a)); + runCheck(0xffa6dcbf, const Color(0x4d82b69b)); // 0x4d82b79b + runCheck(0xff95a5fd, const Color(0x4d95a5fd)); + runCheck(0xff53a063, const Color(0x4d53a063)); + runCheck(0xff9987e1, const Color(0x4d9987e1)); + runCheck(0xffe4523d, const Color(0x4de4523d)); + runCheck(0xffc2c2c2, const Color(0x4dababab)); + runCheck(0xff4f8de4, const Color(0x4d4f8de4)); + runCheck(0xffc6a8ad, const Color(0x4dc2a4a9)); // 0x4dc1a4a9 + runCheck(0xffe7cc4d, const Color(0x4dc3ab2a)); // 0x4dc2aa28 + runCheck(0xffc8bebf, const Color(0x4db3a9aa)); + runCheck(0xffa47462, const Color(0x4da47462)); + + // EXTREME_COLORS + runCheck(0xFFFFFFFF, const Color(0x4dababab)); + runCheck(0xFF000000, const Color(0x4d474747)); + runCheck(0xFFD3D3D3, const Color(0x4dababab)); + runCheck(0xFFA9A9A9, const Color(0x4da9a9a9)); + runCheck(0xFF808080, const Color(0x4d808080)); + runCheck(0xFFFFFF00, const Color(0x4dacb300)); // 0x4dacb200 + runCheck(0xFFFF0000, const Color(0x4dff0000)); + runCheck(0xFF008000, const Color(0x4d008000)); + runCheck(0xFF0000FF, const Color(0x4d0000ff)); // 0x4d0902ff + runCheck(0xFFEE82EE, const Color(0x4dee82ee)); + runCheck(0xFFFFA500, const Color(0x4def9800)); // 0x4ded9600 + runCheck(0xFF800080, const Color(0x4d810181)); // 0x4d810281 + runCheck(0xFF00FFFF, const Color(0x4d00c2c3)); // 0x4d00c3c5 + runCheck(0xFFFF00FF, const Color(0x4dff00ff)); + runCheck(0xFF00FF00, const Color(0x4d00cb00)); + runCheck(0xFF800000, const Color(0x4d8d140c)); // 0x4d8b130b + runCheck(0xFF008080, const Color(0x4d008080)); + runCheck(0xFF000080, const Color(0x4d492bae)); // 0x4d4b2eb3 + runCheck(0xFFFFFFE0, const Color(0x4dadad90)); // 0x4dacad90 + runCheck(0xFFFF69B4, const Color(0x4dff69b4)); + }); + + test('iconOnPlainBackground', () { + void runCheck(int base, Color expected) { + check(streamColorSwatchDark(base)) + [StreamColor.iconOnPlainBackground].equals(expected); + } + + // Check against everything in ZULIP_ASSIGNMENT_COLORS + // in . + // (Skipping `streamColors` because there are 100+ of them.) + // On how to extract expected results from the replit, see: + // https://github.com/zulip/zulip-flutter/pull/643#issuecomment-2093940972 + + // TODO Fix bug causing our implementation's results to differ from the + // replit's. Where they differ, see comment with what the replit gives. + + runCheck(0xff76ce90, const Color(0xff73cb8d)); + runCheck(0xfffae589, const Color(0xffccb95f)); // 0xffcbb85e + runCheck(0xffa6c7e5, const Color(0xff9cbcda)); // 0xff9cbddb + runCheck(0xffe79ab5, const Color(0xffe79ab5)); + runCheck(0xffbfd56f, const Color(0xffacc25d)); + runCheck(0xfff4ae55, const Color(0xfff0ab52)); // 0xffefa951 + runCheck(0xffb0a5fd, const Color(0xffb0a5fd)); + runCheck(0xffaddfe5, const Color(0xff90c1c7)); // 0xff90c2c8 + runCheck(0xfff5ce6e, const Color(0xffd9b456)); // 0xffd8b355 + runCheck(0xffc2726a, const Color(0xffc2726a)); + runCheck(0xff94c849, const Color(0xff94c849)); + runCheck(0xffbd86e5, const Color(0xffbd86e5)); + runCheck(0xffee7e4a, const Color(0xffee7e4a)); + runCheck(0xffa6dcbf, const Color(0xff8fc4a8)); + runCheck(0xff95a5fd, const Color(0xff95a5fd)); + runCheck(0xff53a063, const Color(0xff53a063)); + runCheck(0xff9987e1, const Color(0xff9987e1)); + runCheck(0xffe4523d, const Color(0xffe4523d)); + runCheck(0xffc2c2c2, const Color(0xffb9b9b9)); + runCheck(0xff4f8de4, const Color(0xff4f8de4)); + runCheck(0xffc6a8ad, const Color(0xffc6a8ad)); + runCheck(0xffe7cc4d, const Color(0xffd1b839)); // 0xffd0b737 + runCheck(0xffc8bebf, const Color(0xffc0b6b7)); + runCheck(0xffa47462, const Color(0xffa47462)); + runCheck(0xffacc25d, const Color(0xffacc25d)); + }); + + test('iconOnBarBackground', () { + void runCheck(int base, Color expected) { + check(streamColorSwatchDark(base)) + [StreamColor.iconOnBarBackground].equals(expected); + } + + // Check against everything in ZULIP_ASSIGNMENT_COLORS + // in . + // (Skipping `streamColors` because there are 100+ of them.) + // On how to generate expected results, see: + // https://github.com/zulip/zulip-flutter/pull/643#issuecomment-2093940972 + + // TODO Fix bug causing our implementation's results to differ from the + // web app's. Where they differ, see comment with what web uses. + + runCheck(0xff76ce90, const Color(0xff73cb8d)); + runCheck(0xfffae589, const Color(0xffccb95f)); // 0xffcbb85e + runCheck(0xffa6c7e5, const Color(0xff9cbcda)); // 0xff9cbddb + runCheck(0xffe79ab5, const Color(0xffe79ab5)); + runCheck(0xffbfd56f, const Color(0xffacc25d)); + runCheck(0xfff4ae55, const Color(0xfff0ab52)); // 0xffefa951 + runCheck(0xffb0a5fd, const Color(0xffb0a5fd)); + runCheck(0xffaddfe5, const Color(0xff90c1c7)); // 0xff90c2c8 + runCheck(0xfff5ce6e, const Color(0xffd9b456)); // 0xffd8b355 + runCheck(0xffc2726a, const Color(0xffc2726a)); + runCheck(0xff94c849, const Color(0xff94c849)); + runCheck(0xffbd86e5, const Color(0xffbd86e5)); + runCheck(0xffee7e4a, const Color(0xffee7e4a)); + runCheck(0xffa6dcbf, const Color(0xff8fc4a8)); + runCheck(0xff95a5fd, const Color(0xff95a5fd)); + runCheck(0xff53a063, const Color(0xff53a063)); + runCheck(0xff9987e1, const Color(0xff9987e1)); + runCheck(0xffe4523d, const Color(0xffe4523d)); + runCheck(0xffc2c2c2, const Color(0xffb9b9b9)); + runCheck(0xff4f8de4, const Color(0xff4f8de4)); + runCheck(0xffc6a8ad, const Color(0xffc6a8ad)); + runCheck(0xffe7cc4d, const Color(0xffd1b839)); // 0xffd0b737 + runCheck(0xffc8bebf, const Color(0xffc0b6b7)); + runCheck(0xffa47462, const Color(0xffa47462)); + runCheck(0xffacc25d, const Color(0xffacc25d)); + }); + + test('barBackground', () { + void runCheck(int base, Color expected) { + check(streamColorSwatchDark(base)) + [StreamColor.barBackground].equals(expected); + } + + // Check against everything in ZULIP_ASSIGNMENT_COLORS + // in . + // (Skipping `streamColors` because there are 100+ of them.) + // On how to extract expected results from the replit, see: + // https://github.com/zulip/zulip-flutter/pull/643#issuecomment-2093940972 + + // TODO Fix bug causing our implementation's results to differ from the + // replit's. Where they differ, see comment with what the replit gives. + + runCheck(0xff76ce90, const Color(0xff2e4935)); + runCheck(0xfffae589, const Color(0xff4a4327)); + runCheck(0xffa6c7e5, const Color(0xff3a444e)); // 0xff3a454e + runCheck(0xffe79ab5, const Color(0xff523a42)); + runCheck(0xffbfd56f, const Color(0xff404627)); + runCheck(0xfff4ae55, const Color(0xff563f23)); // 0xff553e23 + runCheck(0xffb0a5fd, const Color(0xff413d59)); + runCheck(0xffaddfe5, const Color(0xff374648)); + runCheck(0xfff5ce6e, const Color(0xff4e4224)); // 0xff4e4124 + runCheck(0xffc2726a, const Color(0xff472d2a)); + runCheck(0xff94c849, const Color(0xff394821)); // 0xff384821 + runCheck(0xffbd86e5, const Color(0xff453351)); + runCheck(0xffee7e4a, const Color(0xff563120)); + runCheck(0xffa6dcbf, const Color(0xff36473e)); + runCheck(0xff95a5fd, const Color(0xff393d59)); + runCheck(0xff53a063, const Color(0xff243c28)); + runCheck(0xff9987e1, const Color(0xff3a3350)); + runCheck(0xffe4523d, const Color(0xff53241c)); // 0xff53241b + runCheck(0xffc2c2c2, const Color(0xff434343)); + runCheck(0xff4f8de4, const Color(0xff263551)); // 0xff253551 + runCheck(0xffc6a8ad, const Color(0xff483e40)); + runCheck(0xffe7cc4d, const Color(0xff4c431d)); // 0xff4c431c + runCheck(0xffc8bebf, const Color(0xff464243)); + runCheck(0xffa47462, const Color(0xff3d2d27)); + runCheck(0xffacc25d, const Color(0xff404627)); + }); + }); }); group('Message', () {