From cf3ef49b999e97db182a9af129d73cbf57446fe6 Mon Sep 17 00:00:00 2001 From: Chris Bobbe Date: Thu, 11 Jul 2024 15:54:43 -0700 Subject: [PATCH] emoji_reaction: Add EmojiReactionTheme, including dark variant Related: #95 --- lib/widgets/emoji_reaction.dart | 128 ++++++++++++++++++++++++++------ lib/widgets/theme.dart | 7 +- 2 files changed, 111 insertions(+), 24 deletions(-) diff --git a/lib/widgets/emoji_reaction.dart b/lib/widgets/emoji_reaction.dart index 24210af8c1..03f8b3921d 100644 --- a/lib/widgets/emoji_reaction.dart +++ b/lib/widgets/emoji_reaction.dart @@ -8,6 +8,104 @@ import 'content.dart'; import 'store.dart'; import 'text.dart'; +/// Emoji-reaction styles that differ between light and dark themes. +class EmojiReactionTheme extends ThemeExtension { + factory EmojiReactionTheme.light() { + return EmojiReactionTheme._( + bgSelected: Colors.white, + + // TODO shadow effect, following web, which uses `box-shadow: inset`: + // https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow#inset + // Needs Flutter support for something like that: + // https://github.com/flutter/flutter/issues/18636 + // https://github.com/flutter/flutter/issues/52999 + // Until then use a solid color; a much-lightened version of the shadow color. + // Also adapt by making [borderUnselected] more transparent, so we'll + // want to check that against web when implementing the shadow. + bgUnselected: const HSLColor.fromAHSL(0.08, 210, 0.50, 0.875).toColor(), + + borderSelected: Colors.black.withOpacity(0.45), + + // TODO see TODO on [bgUnselected] about shadow effect + borderUnselected: Colors.black.withOpacity(0.05), + + textSelected: const HSLColor.fromAHSL(1, 210, 0.20, 0.20).toColor(), + textUnselected: const HSLColor.fromAHSL(1, 210, 0.20, 0.25).toColor(), + ); + } + + factory EmojiReactionTheme.dark() { + return EmojiReactionTheme._( + bgSelected: Colors.black.withOpacity(0.8), + bgUnselected: Colors.black.withOpacity(0.3), + borderSelected: Colors.white.withOpacity(0.75), + borderUnselected: Colors.white.withOpacity(0.15), + textSelected: Colors.white.withOpacity(0.85), + textUnselected: Colors.white.withOpacity(0.75), + ); + } + + EmojiReactionTheme._({ + required this.bgSelected, + required this.bgUnselected, + required this.borderSelected, + required this.borderUnselected, + required this.textSelected, + required this.textUnselected, + }); + + /// The [EmojiReactionTheme] from the context's active theme. + /// + /// The [ThemeData] must include [EmojiReactionTheme] in [ThemeData.extensions]. + static EmojiReactionTheme of(BuildContext context) { + final theme = Theme.of(context); + final extension = theme.extension(); + assert(extension != null); + return extension!; + } + + final Color bgSelected; + final Color bgUnselected; + final Color borderSelected; + final Color borderUnselected; + final Color textSelected; + final Color textUnselected; + + @override + EmojiReactionTheme copyWith({ + Color? bgSelected, + Color? bgUnselected, + Color? borderSelected, + Color? borderUnselected, + Color? textSelected, + Color? textUnselected, + }) { + return EmojiReactionTheme._( + bgSelected: bgSelected ?? this.bgSelected, + bgUnselected: bgUnselected ?? this.bgUnselected, + borderSelected: borderSelected ?? this.borderSelected, + borderUnselected: borderUnselected ?? this.borderUnselected, + textSelected: textSelected ?? this.textSelected, + textUnselected: textUnselected ?? this.textUnselected, + ); + } + + @override + EmojiReactionTheme lerp(EmojiReactionTheme other, double t) { + if (identical(this, other)) { + return this; + } + return EmojiReactionTheme._( + bgSelected: Color.lerp(bgSelected, other.bgSelected, t)!, + bgUnselected: Color.lerp(bgUnselected, other.bgUnselected, t)!, + borderSelected: Color.lerp(borderSelected, other.borderSelected, t)!, + borderUnselected: Color.lerp(borderUnselected, other.borderUnselected, t)!, + textSelected: Color.lerp(textSelected, other.textSelected, t)!, + textUnselected: Color.lerp(textUnselected, other.textUnselected, t)!, + ); + } +} + class ReactionChipsList extends StatelessWidget { const ReactionChipsList({ super.key, @@ -32,24 +130,6 @@ class ReactionChipsList extends StatelessWidget { } } -final _textColorSelected = const HSLColor.fromAHSL(1, 210, 0.20, 0.20).toColor(); -final _textColorUnselected = const HSLColor.fromAHSL(1, 210, 0.20, 0.25).toColor(); - -const _backgroundColorSelected = Colors.white; -// TODO shadow effect, following web, which uses `box-shadow: inset`: -// https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow#inset -// Needs Flutter support for something like that: -// https://github.com/flutter/flutter/issues/18636 -// https://github.com/flutter/flutter/issues/52999 -// Until then use a solid color; a much-lightened version of the shadow color. -// Also adapt by making [_borderColorUnselected] more transparent, so we'll -// want to check that against web when implementing the shadow. -final _backgroundColorUnselected = const HSLColor.fromAHSL(0.08, 210, 0.50, 0.875).toColor(); - -final _borderColorSelected = Colors.black.withOpacity(0.45); -// TODO see TODO on [_backgroundColorUnselected] about shadow effect -final _borderColorUnselected = Colors.black.withOpacity(0.05); - class ReactionChip extends StatelessWidget { final bool showName; final int messageId; @@ -85,10 +165,11 @@ class ReactionChip extends StatelessWidget { }).join(', ') : userIds.length.toString(); - final borderColor = selfVoted ? _borderColorSelected : _borderColorUnselected; - final labelColor = selfVoted ? _textColorSelected : _textColorUnselected; - final backgroundColor = selfVoted ? _backgroundColorSelected : _backgroundColorUnselected; - final splashColor = selfVoted ? _backgroundColorUnselected : _backgroundColorSelected; + final reactionTheme = EmojiReactionTheme.of(context); + final borderColor = selfVoted ? reactionTheme.borderSelected : reactionTheme.borderUnselected; + final labelColor = selfVoted ? reactionTheme.textSelected : reactionTheme.textUnselected; + final backgroundColor = selfVoted ? reactionTheme.bgSelected : reactionTheme.bgUnselected; + final splashColor = selfVoted ? reactionTheme.bgUnselected : reactionTheme.bgSelected; final highlightColor = splashColor.withOpacity(0.5); final borderSide = BorderSide( @@ -349,6 +430,7 @@ class _TextEmoji extends StatelessWidget { @override Widget build(BuildContext context) { + final reactionTheme = EmojiReactionTheme.of(context); return Text( textAlign: TextAlign.end, textScaler: _textEmojiScalerClamped(context), @@ -356,7 +438,7 @@ class _TextEmoji extends StatelessWidget { style: TextStyle( fontSize: 14 * 0.8, height: 1, // to be denser when we have to wrap - color: selected ? _textColorSelected : _textColorUnselected, + color: selected ? reactionTheme.textSelected : reactionTheme.textUnselected, ).merge(weightVariableTextStyle(context, wght: selected ? 600 : null)), // Encourage line breaks before "_" (common in these), but try not diff --git a/lib/widgets/theme.dart b/lib/widgets/theme.dart index ac6ff28f13..e747da9610 100644 --- a/lib/widgets/theme.dart +++ b/lib/widgets/theme.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../api/model/model.dart'; import 'content.dart'; +import 'emoji_reaction.dart'; import 'stream_colors.dart'; import 'text.dart'; @@ -9,7 +10,11 @@ ThemeData zulipThemeData(BuildContext context) { final designVariables = DesignVariables(); return ThemeData( typography: zulipTypography(context), - extensions: [ContentTheme.light(context), designVariables], + extensions: [ + ContentTheme.light(context), + designVariables, + EmojiReactionTheme.light(), + ], appBarTheme: AppBarTheme( // Set these two fields to prevent a color change in [AppBar]s when // there is something scrolled under it. If an app bar hasn't been