2464 the empty space above the message is not pleasing any solution (#2475)

* chore: shrink message token buttons when empty

* chore: fix message overlay overflow error
pull/1817/head
ggurdin 7 months ago committed by GitHub
parent 92078265a4
commit e5d839b20f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -315,7 +315,7 @@ class HtmlMessage extends StatelessWidget {
), ),
), ),
width: tokenWidth, width: tokenWidth,
animate: isTransitionAnimation, animateIn: isTransitionAnimation,
practiceTarget: practiceTarget:
overlayController?.toolbarMode.associatedActivityType != overlayController?.toolbarMode.associatedActivityType !=
null null

@ -32,10 +32,6 @@ class ChoiceAnimationWidgetState extends State<ChoiceAnimationWidget>
duration: const Duration(milliseconds: choiceArrayAnimationDuration), duration: const Duration(milliseconds: choiceArrayAnimationDuration),
vsync: this, vsync: this,
); );
if (widget.isSelected) {
_controller.forward().then((_) => _controller.reset());
}
} }
@override @override

@ -25,14 +25,13 @@ const double tokenButtonHeight = 40.0;
const double tokenButtonDefaultFontSize = 10; const double tokenButtonDefaultFontSize = 10;
const int maxEmojisPerLemma = 1; const int maxEmojisPerLemma = 1;
const double estimatedEmojiWidthRatio = 2; const double estimatedEmojiWidthRatio = 2;
const double estimatedEmojiHeightRatio = 1.3;
class MessageTokenButton extends StatefulWidget { class MessageTokenButton extends StatefulWidget {
final MessageOverlayController? overlayController; final MessageOverlayController? overlayController;
final PangeaToken token; final PangeaToken token;
final TextStyle textStyle; final TextStyle textStyle;
final double width; final double width;
final bool animate; final bool animateIn;
final PracticeTarget? practiceTarget; final PracticeTarget? practiceTarget;
const MessageTokenButton({ const MessageTokenButton({
@ -42,7 +41,7 @@ class MessageTokenButton extends StatefulWidget {
required this.textStyle, required this.textStyle,
required this.width, required this.width,
required this.practiceTarget, required this.practiceTarget,
this.animate = false, this.animateIn = false,
}); });
@override @override
@ -59,14 +58,20 @@ class MessageTokenButtonState extends State<MessageTokenButton>
late Animation<double> _iconSizeAnimation; late Animation<double> _iconSizeAnimation;
bool _isHovered = false; bool _isHovered = false;
bool _isSelected = false;
bool _finishedInitialAnimation = false;
bool _wasEmpty = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_setSelected();
_controller = AnimationController( _controller = AnimationController(
vsync: this, vsync: this,
duration: const Duration( duration: const Duration(
milliseconds: AppConfig.overlayAnimationDuration, milliseconds: AppConfig.overlayAnimationDuration,
// seconds: 5,
), ),
); );
@ -88,29 +93,29 @@ class MessageTokenButtonState extends State<MessageTokenButton>
CurvedAnimation(parent: _iconSizeController, curve: Curves.easeInOut), CurvedAnimation(parent: _iconSizeController, curve: Curves.easeInOut),
); );
if (widget.animate) { _wasEmpty = _isEmpty;
_controller.forward();
if (!_isEmpty) {
_controller.forward().then((_) {
if (mounted) setState(() => _finishedInitialAnimation = true);
});
} else {
setState(() => _finishedInitialAnimation = true);
} }
} }
double get topPadding => 10.0;
double get height =>
widget.animate ? _heightAnimation.value : tokenButtonHeight;
@override @override
void didUpdateWidget(covariant MessageTokenButton oldWidget) { void didUpdateWidget(covariant MessageTokenButton oldWidget) {
if (oldWidget.overlayController?.toolbarMode !=
widget.overlayController?.toolbarMode ||
oldWidget.overlayController?.selectedToken !=
widget.overlayController?.selectedToken ||
oldWidget.overlayController?.selectedMorph !=
widget.overlayController?.selectedMorph ||
widget.token.vocabConstructID.constructUses.points !=
widget.token.vocabConstructID.constructUses.points) {
setState(() {});
}
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
_setSelected();
if (_isEmpty != _wasEmpty) {
if (_isEmpty && _animate) {
_controller.reverse();
} else if (!_isEmpty && _animate) {
_controller.forward();
}
setState(() => _wasEmpty = _isEmpty);
}
} }
@override @override
@ -120,18 +125,51 @@ class MessageTokenButtonState extends State<MessageTokenButton>
super.dispose(); super.dispose();
} }
double get textSize => bool get _animate => widget.animateIn || _finishedInitialAnimation;
widget.textStyle.fontSize ?? tokenButtonDefaultFontSize;
double get emojiSize => textSize * estimatedEmojiWidthRatio; PracticeTarget? get _activity => widget.practiceTarget;
TextStyle get emojiStyle => widget.textStyle.copyWith( bool get _isActivityCompleteForToken =>
fontSize: textSize + 4, _activity?.isCompleteByToken(
); widget.token,
_activity!.morphFeature,
) ==
true;
void _setSelected() {
final selected =
widget.overlayController?.selectedMorph?.token == widget.token &&
widget.overlayController?.selectedMorph?.morph ==
_activity?.morphFeature;
PracticeTarget? get activity => widget.practiceTarget; if (selected != _isSelected) {
setState(() {
_isSelected = selected;
});
_isSelected
? _iconSizeController.forward()
: _iconSizeController.reverse();
}
}
void _setHovered(bool isHovered) {
if (isHovered != _isHovered) {
setState(() {
_isHovered = isHovered;
});
if (!_isHovered && _isSelected) {
return;
}
onMatch(PracticeChoice form) { _isHovered
? _iconSizeController.forward()
: _iconSizeController.reverse();
}
}
void _onMatch(PracticeChoice form) {
if (widget.overlayController?.activity == null) { if (widget.overlayController?.activity == null) {
debugger(when: kDebugMode); debugger(when: kDebugMode);
ErrorHandler.logError( ErrorHandler.logError(
@ -140,9 +178,7 @@ class MessageTokenButtonState extends State<MessageTokenButton>
); );
return; return;
} }
widget.overlayController!.selectedChoice = null; widget.overlayController!.onChoiceSelect(null);
widget.overlayController!.setState(() {});
widget.overlayController!.activity!.onMatch( widget.overlayController!.activity!.onMatch(
widget.token, widget.token,
form, form,
@ -151,65 +187,136 @@ class MessageTokenButtonState extends State<MessageTokenButton>
); );
} }
Widget get emojiView { bool get _isEmpty {
// if (widget.token.text.content.length == 1 || maxEmojisPerLemma == 1) { final mode = widget.overlayController?.toolbarMode;
return ShrinkableText( return _activity == null ||
text: widget.token.vocabConstructID.userSetEmoji.firstOrNull ?? '', (_isActivityCompleteForToken &&
maxWidth: widget.width, ![MessageMode.wordEmoji, MessageMode.wordMorph].contains(mode)) ||
style: emojiStyle, (MessageMode.wordMorph == mode && _activity?.morphFeature == null);
}
@override
Widget build(BuildContext context) {
if (widget.overlayController == null) {
return const SizedBox.shrink();
}
if (!_animate) {
return MessageTokenButtonContent(
activity: _activity,
messageMode: widget.overlayController!.toolbarMode,
token: widget.token,
selectedChoice: widget.overlayController?.selectedChoice,
isComplete: _isActivityCompleteForToken,
isSelected: _isSelected,
height: tokenButtonHeight,
width: widget.width,
textStyle: widget.textStyle,
sizeAnimation: _iconSizeAnimation,
onHover: _setHovered,
onTap: () => widget.overlayController!.onMorphActivitySelect(
MorphSelection(widget.token, _activity!.morphFeature!),
),
onMatch: _onMatch,
); );
// }
// return Stack(
// alignment: Alignment.center,
// children: widget.token.vocabConstructID.userSetEmoji
// .take(maxEmojisPerLemma)
// .mapIndexed(
// (index, emoji) => Positioned(
// left: min(
// index /
// widget.token.vocabConstructID.userSetEmoji.length *
// totalAvailableWidth,
// index * emojiSize,
// ),
// child: Text(
// emoji,
// style: emojiStyle,
// ),
// ),
// )
// .toList()
// .reversed
// .toList(),
// );
} }
bool get isActivityCompleteForToken => return AnimatedBuilder(
activity?.isCompleteByToken( animation: _heightAnimation,
widget.token, builder: (context, child) {
activity!.morphFeature, return MessageTokenButtonContent(
) == activity: _activity,
true; messageMode: widget.overlayController!.toolbarMode,
token: widget.token,
selectedChoice: widget.overlayController?.selectedChoice,
isComplete: _isActivityCompleteForToken,
isSelected: _isSelected,
height: _heightAnimation.value,
width: widget.width,
textStyle: widget.textStyle,
sizeAnimation: _iconSizeAnimation,
onHover: _setHovered,
onTap: () => widget.overlayController!.onMorphActivitySelect(
MorphSelection(widget.token, _activity!.morphFeature!),
),
onMatch: _onMatch,
);
},
);
}
}
Color get color { class MessageTokenButtonContent extends StatelessWidget {
final PracticeTarget? activity;
final MessageMode messageMode;
final PangeaToken token;
final PracticeChoice? selectedChoice;
final bool isComplete;
final bool isSelected;
final double height;
final double width;
final TextStyle textStyle;
final Animation<double> sizeAnimation;
final Function(bool)? onHover;
final Function()? onTap;
final Function(PracticeChoice)? onMatch;
const MessageTokenButtonContent({
super.key,
required this.activity,
required this.messageMode,
required this.token,
required this.selectedChoice,
required this.isComplete,
required this.isSelected,
required this.height,
required this.width,
required this.textStyle,
required this.sizeAnimation,
this.onHover,
this.onTap,
this.onMatch,
});
TextStyle get _emojiStyle => textStyle.copyWith(
fontSize: (textStyle.fontSize ?? tokenButtonDefaultFontSize) + 4,
);
static final _borderRadius =
BorderRadius.circular(AppConfig.borderRadius - 4);
Color _color(BuildContext context) {
if (activity == null) { if (activity == null) {
return Theme.of(context).colorScheme.primary; return Theme.of(context).colorScheme.primary;
} }
if (isActivityCompleteForToken) { if (isComplete) {
return AppConfig.gold; return AppConfig.gold;
} }
return Theme.of(context).colorScheme.primary; return Theme.of(context).colorScheme.primary;
} }
Widget get content { @override
final tokenActivity = activity; Widget build(BuildContext context) {
if (tokenActivity == null || isActivityCompleteForToken) { if (activity == null) {
if (MessageMode.wordEmoji == widget.overlayController?.toolbarMode) { return SizedBox(height: height);
return SizedBox(height: height, child: emojiView); }
if (isComplete) {
if (MessageMode.wordEmoji == messageMode) {
return SizedBox(
height: height,
child: ShrinkableText(
text: token.vocabConstructID.userSetEmoji.firstOrNull ?? '',
maxWidth: width,
style: _emojiStyle,
),
);
} }
if (MessageMode.wordMorph == widget.overlayController?.toolbarMode && if (MessageMode.wordMorph == messageMode) {
activity?.morphFeature != null) {
final morphFeature = activity!.morphFeature!; final morphFeature = activity!.morphFeature!;
final morphTag = widget.token.morphIdByFeature(morphFeature); final morphTag = token.morphIdByFeature(morphFeature);
if (morphTag != null) { if (morphTag != null) {
return Tooltip( return Tooltip(
message: getGrammarCopy( message: getGrammarCopy(
@ -218,7 +325,7 @@ class MessageTokenButtonState extends State<MessageTokenButton>
context: context, context: context,
), ),
child: SizedBox( child: SizedBox(
width: widget.width, width: width,
height: height, height: height,
child: Center( child: Center(
child: MorphIcon( child: MorphIcon(
@ -234,43 +341,28 @@ class MessageTokenButtonState extends State<MessageTokenButton>
return SizedBox(height: height); return SizedBox(height: height);
} }
if (MessageMode.wordMorph == widget.overlayController?.toolbarMode) { if (MessageMode.wordMorph == messageMode) {
if (activity?.morphFeature == null) { if (activity?.morphFeature == null) {
return SizedBox(height: height); return SizedBox(height: height);
} }
final bool isSelected =
(widget.overlayController?.selectedMorph?.token == widget.token &&
widget.overlayController?.selectedMorph?.morph ==
activity?.morphFeature) ||
_isHovered;
// Trigger the icon size animation based on hover or selection
if (isSelected) {
_iconSizeController.forward();
} else {
_iconSizeController.reverse();
}
return InkWell( return InkWell(
onHover: (isHovered) => setState(() => _isHovered = isHovered), onHover: onHover,
onTap: () => widget.overlayController!.onMorphActivitySelect( onTap: onTap,
MorphSelection(widget.token, activity!.morphFeature!), borderRadius: _borderRadius,
),
borderRadius: borderRadius,
child: Container( child: Container(
height: height, height: height,
width: min(widget.width, height), width: min(width, height),
alignment: Alignment.center, alignment: Alignment.center,
child: Opacity( child: Opacity(
opacity: isSelected ? 1.0 : 0.4, opacity: isSelected ? 1.0 : 0.4,
child: AnimatedBuilder( child: AnimatedBuilder(
animation: _iconSizeAnimation, animation: sizeAnimation,
builder: (context, child) { builder: (context, child) {
return Icon( return Icon(
Symbols.toys_and_games, Symbols.toys_and_games,
color: color, color: _color(context),
size: _iconSizeAnimation.value, // Use the new animation size: sizeAnimation.value, // Use the new animation
); );
}, },
), ),
@ -282,61 +374,41 @@ class MessageTokenButtonState extends State<MessageTokenButton>
return DragTarget<PracticeChoice>( return DragTarget<PracticeChoice>(
builder: (BuildContext context, accepted, rejected) { builder: (BuildContext context, accepted, rejected) {
final double colorAlpha = 0.3 + final double colorAlpha = 0.3 +
(widget.overlayController?.selectedChoice != null ? 0.4 : 0.0) + (selectedChoice != null ? 0.4 : 0.0) +
(accepted.isNotEmpty || _isHovered ? 0.3 : 0.0); (accepted.isNotEmpty ? 0.3 : 0.0);
return InkWell( return InkWell(
onHover: (isHovered) => setState(() => _isHovered = isHovered), onTap: selectedChoice != null
onTap: widget.overlayController?.selectedChoice != null ? () => onMatch?.call(selectedChoice!)
? () => onMatch(widget.overlayController!.selectedChoice!)
: null, : null,
borderRadius: borderRadius, borderRadius: _borderRadius,
child: CustomPaint( child: CustomPaint(
painter: DottedBorderPainter( painter: DottedBorderPainter(
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.primary .primary
.withAlpha((colorAlpha * 255).toInt()), .withAlpha((colorAlpha * 255).toInt()),
borderRadius: borderRadius, borderRadius: _borderRadius,
), ),
child: Container( child: Container(
height: height, height: height,
padding: EdgeInsets.only(top: topPadding), padding: const EdgeInsets.only(top: 10.0),
width: MessageMode.wordMeaning == width: MessageMode.wordMeaning == messageMode
widget.overlayController?.toolbarMode ? width
? widget.width : min(width, height),
: min(widget.width, height),
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.primary .primary
.withAlpha((max(0, colorAlpha - 0.7) * 255).toInt()), .withAlpha((max(0, colorAlpha - 0.7) * 255).toInt()),
borderRadius: borderRadius, borderRadius: _borderRadius,
), ),
), ),
), ),
); );
}, },
onAcceptWithDetails: (details) => onMatch(details.data), onAcceptWithDetails: (details) => onMatch?.call(details.data),
);
}
static final borderRadius = BorderRadius.circular(AppConfig.borderRadius - 4);
@override
Widget build(BuildContext context) {
if (widget.overlayController == null) {
return const SizedBox.shrink();
}
if (!widget.animate) {
return content;
}
return AnimatedBuilder(
animation: _heightAnimation,
builder: (context, child) => content,
); );
} }
} }

@ -35,10 +35,6 @@ class MatchActivityCard extends StatelessWidget {
) { ) {
switch (activityType) { switch (activityType) {
case ActivityTypeEnum.emoji: case ActivityTypeEnum.emoji:
return Text(
choice,
style: TextStyle(fontSize: fontSize),
);
case ActivityTypeEnum.wordMeaning: case ActivityTypeEnum.wordMeaning:
return Text( return Text(
choice, choice,

@ -107,9 +107,19 @@ class PracticeMatchItemState extends State<PracticeMatchItem> {
} }
} }
IntrinsicWidth content(BuildContext context) { void onTap() {
return IntrinsicWidth( play();
child: Container( isCorrect == null || !isCorrect! || widget.token == null
? widget.overlayController.onChoiceSelect(widget.constructForm)
: widget.overlayController.updateSelectedSpan(widget.token!.text);
}
@override
Widget build(BuildContext context) {
final content = Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
height: widget.fixedSize, height: widget.fixedSize,
width: widget.fixedSize, width: widget.fixedSize,
alignment: Alignment.center, alignment: Alignment.center,
@ -129,23 +139,14 @@ class PracticeMatchItemState extends State<PracticeMatchItem> {
), ),
child: widget.content, child: widget.content,
), ),
],
); );
}
void onTap() {
play();
isCorrect == null || !isCorrect! || widget.token == null
? widget.overlayController.onChoiceSelect(widget.constructForm)
: widget.overlayController.updateSelectedSpan(widget.token!.text);
}
@override
Widget build(BuildContext context) {
return LongPressDraggable<PracticeChoice>( return LongPressDraggable<PracticeChoice>(
data: widget.constructForm, data: widget.constructForm,
feedback: Material( feedback: Material(
type: MaterialType.transparency, type: MaterialType.transparency,
child: content(context), child: content,
), ),
delay: const Duration(milliseconds: 50), delay: const Duration(milliseconds: 50),
onDragStarted: onTap, onDragStarted: onTap,
@ -153,7 +154,7 @@ class PracticeMatchItemState extends State<PracticeMatchItem> {
onHover: (isHovered) => setState(() => _isHovered = isHovered), onHover: (isHovered) => setState(() => _isHovered = isHovered),
borderRadius: BorderRadius.circular(AppConfig.borderRadius), borderRadius: BorderRadius.circular(AppConfig.borderRadius),
onTap: onTap, onTap: onTap,
child: content(context), child: content,
), ),
); );
} }

@ -370,7 +370,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
} }
} }
void onChoiceSelect(PracticeChoice choice, [bool force = false]) { void onChoiceSelect(PracticeChoice? choice, [bool force = false]) {
if (selectedChoice == choice && !force) { if (selectedChoice == choice && !force) {
selectedChoice = null; selectedChoice = null;
} else { } else {

@ -145,6 +145,8 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
} }
void _setCenteredMessageSize(RenderBox renderBox) { void _setCenteredMessageSize(RenderBox renderBox) {
if (_centeredMessageCompleter.isCompleted) return;
_centeredMessageSize = renderBox.size; _centeredMessageSize = renderBox.size;
final offset = renderBox.localToGlobal(Offset.zero); final offset = renderBox.localToGlobal(Offset.zero);
_centeredMessageOffset = Offset( _centeredMessageOffset = Offset(
@ -406,12 +408,17 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
if (hasHeaderOverflow) { if (hasHeaderOverflow) {
final difference = topOffset - (_headerHeight + AppConfig.toolbarSpacing); final difference = topOffset - (_headerHeight + AppConfig.toolbarSpacing);
return Offset( double newBottomOffset = _mediaQuery!.size.height -
_ownMessage ? _messageRightOffset : _messageLeftOffset,
_mediaQuery!.size.height -
_originalMessageOffset.dy + _originalMessageOffset.dy +
difference - difference -
_originalMessageSize.height, _originalMessageSize.height;
if (newBottomOffset < _footerHeight + AppConfig.toolbarSpacing) {
newBottomOffset = _footerHeight + AppConfig.toolbarSpacing;
}
return Offset(
_ownMessage ? _messageRightOffset : _messageLeftOffset,
newBottomOffset,
); );
} else { } else {
final difference = final difference =

@ -225,7 +225,7 @@ class MessageTextWidget extends StatelessWidget {
overlayController: overlayController, overlayController: overlayController,
textStyle: renderer.style(context), textStyle: renderer.style(context),
width: tokenWidth, width: tokenWidth,
animate: isTransitionAnimation, animateIn: isTransitionAnimation,
practiceTarget: overlayController practiceTarget: overlayController
?.toolbarMode.associatedActivityType != ?.toolbarMode.associatedActivityType !=
null null

@ -312,7 +312,7 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
if (activityWidget != null) activityWidget!, if (activityWidget != null && !fetchingActivity) activityWidget!,
// Conditionally show the darkening and progress indicator based on the loading state // Conditionally show the darkening and progress indicator based on the loading state
if (!savoringTheJoy && fetchingActivity) ...[ if (!savoringTheJoy && fetchingActivity) ...[
// Circular progress indicator in the center // Circular progress indicator in the center

Loading…
Cancel
Save