diff --git a/lib/pages/dynamics/widgets/vote.dart b/lib/pages/dynamics/widgets/vote.dart index be9574212..0c15ac67d 100644 --- a/lib/pages/dynamics/widgets/vote.dart +++ b/lib/pages/dynamics/widgets/vote.dart @@ -10,6 +10,7 @@ import 'package:PiliPlus/models/common/badge_type.dart'; import 'package:PiliPlus/models/common/image_preview_type.dart'; import 'package:PiliPlus/models/dynamics/vote_model.dart'; import 'package:PiliPlus/models_new/followee_votes/vote.dart'; +import 'package:PiliPlus/pages/dynamics/widgets/vote_decoration.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/grid.dart'; @@ -84,7 +85,7 @@ class _VotePanelState extends State { ), Flexible( child: Padding( - padding: const EdgeInsets.symmetric(vertical: 6), + padding: const .symmetric(vertical: 8), child: _voteInfo.type == 1 ? GridView.builder( shrinkWrap: true, @@ -105,7 +106,7 @@ class _VotePanelState extends State { itemCount: _voteInfo.options.length, itemBuilder: (context, index) => _buildOptions(index), separatorBuilder: (context, index) => - const SizedBox(height: 6), + const SizedBox(height: 8), ), ), ), @@ -428,7 +429,7 @@ class _VotePanelState extends State { selected: selected, onSelected: !_enabled ? null - : (value) => _onSelected(context, value, opt.optIdx!), + : () => _onSelected(context, !selected, opt.optIdx!), ); }, ); @@ -464,7 +465,7 @@ class PercentageChip extends StatelessWidget { final String label; final double? percentage; final bool selected; - final ValueChanged? onSelected; + final VoidCallback? onSelected; const PercentageChip({ super.key, @@ -474,48 +475,41 @@ class PercentageChip extends StatelessWidget { this.percentage, }); + static final EdgeInsets _padding = PlatformUtils.isMobile + ? const .symmetric(horizontal: 12, vertical: 10) + : const .symmetric(horizontal: 12, vertical: 8); + @override Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - return ChoiceChip( - tooltip: label, - labelPadding: EdgeInsets.zero, - padding: EdgeInsets.zero, - showCheckmark: false, - clipBehavior: Clip.hardEdge, - label: Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - if (percentage != null) - Positioned.fill( - left: 0, - child: FractionallySizedBox( - alignment: Alignment.centerLeft, - widthFactor: percentage, - child: ColoredBox( - color: selected - ? colorScheme.inversePrimary - : colorScheme.outlineVariant, - ), - ), + final colorScheme = ColorScheme.of(context); + return Tooltip( + message: label, + child: Material( + borderRadius: const .all(.circular(8)), + color: selected ? colorScheme.secondaryContainer : null, + child: InkWell( + onTap: onSelected, + borderRadius: const .all(.circular(8)), + child: Container( + padding: _padding, + decoration: VoteDecoration( + borderRadius: const .all(.circular(8)), + border: .all(color: colorScheme.outlineVariant), + percentage: percentage ?? 0, + color: selected + ? colorScheme.inversePrimary + : colorScheme.outlineVariant, ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), child: Row( spacing: 8, children: [ Expanded( child: Row( spacing: 4, - mainAxisSize: MainAxisSize.min, + mainAxisSize: .min, children: [ Flexible( - child: Text( - label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + child: Text(label, maxLines: 1, overflow: .ellipsis), ), if (selected) Icon( @@ -531,10 +525,8 @@ class PercentageChip extends StatelessWidget { ], ), ), - ], + ), ), - selected: selected, - onSelected: onSelected, ); } } diff --git a/lib/pages/dynamics/widgets/vote_decoration.dart b/lib/pages/dynamics/widgets/vote_decoration.dart new file mode 100644 index 000000000..479b87fd5 --- /dev/null +++ b/lib/pages/dynamics/widgets/vote_decoration.dart @@ -0,0 +1,177 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; + +class VoteDecoration extends Decoration { + const VoteDecoration({ + required this.color, + required this.percentage, + required this.border, + required this.borderRadius, + }); + + final Color color; + final double percentage; + final BoxBorder border; + final BorderRadius borderRadius; + + @override + EdgeInsetsGeometry get padding => border.dimensions; + + @override + Path getClipPath(Rect rect, TextDirection textDirection) { + return Path()..addRRect(borderRadius.resolve(textDirection).toRRect(rect)); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is VoteDecoration && + other.color == color && + other.border == border && + other.borderRadius == borderRadius && + other.percentage == percentage; + } + + @override + int get hashCode => Object.hash(color, border, borderRadius, percentage); + + @override + bool hitTest(Size size, Offset position, {TextDirection? textDirection}) { + assert((Offset.zero & size).contains(position)); + final Offset center = size.center(Offset.zero); + final double distance = (position - center).distance; + return distance <= math.min(size.width, size.height) / 2.0; + } + + @override + BoxPainter createBoxPainter([VoidCallback? onChanged]) { + assert(onChanged != null); + return _VoteDecorationPainter(this, onChanged); + } +} + +class _VoteDecorationPainter extends BoxPainter { + _VoteDecorationPainter(this._decoration, super.onChanged); + + final VoteDecoration _decoration; + + Paint? _cachedBackgroundPaint; + Rect? _rectForCachedBackgroundPaint; + Paint _getBackgroundPaint(Rect rect, TextDirection? textDirection) { + assert( + _rectForCachedBackgroundPaint == null, + ); + + if (_cachedBackgroundPaint == null) { + final paint = Paint()..color = _decoration.color; + _cachedBackgroundPaint = paint; + } + + return _cachedBackgroundPaint!; + } + + void _paintBox( + Canvas canvas, + Rect rect, + Paint paint, + TextDirection? textDirection, + ) { + if (_decoration.percentage == 1.0) { + canvas.drawRRect( + _decoration.borderRadius.resolve(textDirection).toRRect(rect), + paint, + ); + } else { + final borderRadius = BorderRadius.horizontal( + left: _decoration.borderRadius.topLeft, + ); + canvas.drawRRect( + borderRadius.resolve(textDirection).toRRect(rect), + paint, + ); + } + } + + void _paintBackgroundColor( + Canvas canvas, + Rect rect, + TextDirection? textDirection, + ) { + final Rect adjustedRect = _adjustedRectOnOutlinedBorder( + rect, + textDirection, + ); + _paintBox( + canvas, + adjustedRect, + _getBackgroundPaint(rect, textDirection), + textDirection, + ); + } + + double _calculateAdjustedSide(BorderSide side) { + if (side.color.alpha == 255 && side.style == .solid) { + return side.strokeInset; + } + return 0; + } + + Rect _adjustedRectOnOutlinedBorder(Rect rect, TextDirection? textDirection) { + final border = _decoration.border as Border; + + final EdgeInsets insets = + EdgeInsets.fromLTRB( + _calculateAdjustedSide(border.left), + _calculateAdjustedSide(border.top), + _calculateAdjustedSide(border.right), + _calculateAdjustedSide(border.bottom), + ) / + 2; + + return Rect.fromLTRB( + rect.left + insets.left, + rect.top + insets.top, + rect.right - insets.right, + rect.bottom - insets.bottom, + ); + } + + @override + void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { + assert(configuration.size != null); + final Rect rect = offset & configuration.size!; + final TextDirection? textDirection = configuration.textDirection; + if (_decoration.percentage > 0) { + final Rect bgRect; + if (_decoration.percentage == 1.0) { + bgRect = rect; + } else { + bgRect = + offset & + Size( + configuration.size!.width * (_decoration.percentage), + configuration.size!.height, + ); + } + _paintBackgroundColor(canvas, bgRect, textDirection); + } + _decoration.border.paint( + canvas, + rect, + shape: .rectangle, + borderRadius: _decoration.borderRadius.resolve(textDirection), + textDirection: configuration.textDirection, + ); + } + + @override + String toString() { + return 'VotePainter for $_decoration'; + } +}