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,
animate: isTransitionAnimation,
animateIn: isTransitionAnimation,
practiceTarget:
overlayController?.toolbarMode.associatedActivityType !=
null

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

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

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

@ -107,31 +107,6 @@ class PracticeMatchItemState extends State<PracticeMatchItem> {
}
}
IntrinsicWidth content(BuildContext context) {
return IntrinsicWidth(
child: Container(
height: widget.fixedSize,
width: widget.fixedSize,
alignment: Alignment.center,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color(context).withAlpha((0.4 * 255).toInt()),
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
border: isSelected
? Border.all(
color: color(context),
width: 2,
)
: Border.all(
color: Colors.transparent,
width: 2,
),
),
child: widget.content,
),
);
}
void onTap() {
play();
isCorrect == null || !isCorrect! || widget.token == null
@ -141,11 +116,37 @@ class PracticeMatchItemState extends State<PracticeMatchItem> {
@override
Widget build(BuildContext context) {
final content = Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
height: widget.fixedSize,
width: widget.fixedSize,
alignment: Alignment.center,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color(context).withAlpha((0.4 * 255).toInt()),
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
border: isSelected
? Border.all(
color: color(context),
width: 2,
)
: Border.all(
color: Colors.transparent,
width: 2,
),
),
child: widget.content,
),
],
);
return LongPressDraggable<PracticeChoice>(
data: widget.constructForm,
feedback: Material(
type: MaterialType.transparency,
child: content(context),
child: content,
),
delay: const Duration(milliseconds: 50),
onDragStarted: onTap,
@ -153,7 +154,7 @@ class PracticeMatchItemState extends State<PracticeMatchItem> {
onHover: (isHovered) => setState(() => _isHovered = isHovered),
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
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) {
selectedChoice = null;
} else {

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

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

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

Loading…
Cancel
Save