Skip to content

Commit

Permalink
feat: add tooltip widget (#58)
Browse files Browse the repository at this point in the history
* adjusting layout

* semantics and test

* changelog
  • Loading branch information
erickzanardo authored Jun 18, 2023
1 parent 881c265 commit b7c8e1b
Show file tree
Hide file tree
Showing 8 changed files with 380 additions and 8 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- fix: `NesTabView` didn't keept state on tab change.
- feat: add `NesIcons.exclamationMarkBlock`
- feat: add `NesIcons.questionMarkBlock`
- feat: add `NesTooltip`

# 0.6.0

Expand Down
2 changes: 2 additions & 0 deletions example/lib/gallery/gallery_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ class GalleryPage extends StatelessWidget {
const SizedBox(height: 32),
const TextSection(),
const SizedBox(height: 32),
const TooltipsSection(),
const SizedBox(height: 32),
const DropShadowsSection(),
const SizedBox(height: 32),
const WindowsSection(),
Expand Down
1 change: 1 addition & 0 deletions example/lib/gallery/sections/sections.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ export 'split_panel.dart';
export 'tab_view.dart';
export 'text_fields.dart';
export 'texts.dart';
export 'tooltips.dart';
export 'windows.dart';
46 changes: 46 additions & 0 deletions example/lib/gallery/sections/tooltips.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:nes_ui/nes_ui.dart';

class TooltipsSection extends StatelessWidget {
const TooltipsSection({super.key});

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Tooltips',
style: theme.textTheme.displayMedium,
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
NesTooltip(
message: 'This is a tooltip',
arrowPlacement: NesTooltipArrowPlacement.left,
child: NesIcon(
iconData: NesIcons.instance.exclamationMarkBlock,
),
),
NesTooltip(
message: 'This is a tooltip',
child: NesIcon(
iconData: NesIcons.instance.exclamationMarkBlock,
),
),
NesTooltip(
message: 'This is a tooltip',
arrowPlacement: NesTooltipArrowPlacement.right,
child: NesIcon(
iconData: NesIcons.instance.exclamationMarkBlock,
),
),
],
),
],
);
}
}
75 changes: 67 additions & 8 deletions lib/src/theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,57 @@ class NesSnackbarTheme extends ThemeExtension<NesSnackbarTheme> {
}
}

/// {@template nes_tooltip_theme}
/// Class with information regarding tooltips inside NesUI.
/// {@endtemplate}
class NesTooltipTheme extends ThemeExtension<NesTooltipTheme> {
/// {@macro nes_tooltip_theme}
const NesTooltipTheme({
required this.background,
required this.textColor,
});

/// The color for the tooltip background.
/// Defaults to TextTheme.bodyMedium.
final Color background;

/// The color for the text message.
/// Defaults to Theme.background.
final Color textColor;

@override
NesTooltipTheme copyWith({
Color? background,
Color? textColor,
}) {
return NesTooltipTheme(
background: background ?? this.background,
textColor: textColor ?? this.textColor,
);
}

@override
ThemeExtension<NesTooltipTheme> lerp(
ThemeExtension<NesTooltipTheme>? other,
double t,
) {
final otherExt = other as NesTooltipTheme?;

return NesTooltipTheme(
background: ColorTween(
begin: background,
end: otherExt?.background,
).lerp(t) ??
background,
textColor: ColorTween(
begin: textColor,
end: otherExt?.textColor,
).lerp(t) ??
textColor,
);
}
}

/// Helper methods on [BuildContext] for the Flutter Nes.
extension NesBuildContext on BuildContext {
/// Returns the extension of type [T] from the context.
Expand Down Expand Up @@ -387,6 +438,7 @@ ThemeData flutterNesTheme({
warning: Color(0xfff7d51d),
error: Color(0xffe76e55),
),
NesTooltipTheme? nesTooltipTheme,
Iterable<ThemeExtension<dynamic>> customExtensions = const [],
}) {
final iconTheme = nesIconTheme ??
Expand All @@ -408,23 +460,30 @@ ThemeData flutterNesTheme({
final themeData = ThemeData(
brightness: brightness,
colorSchemeSeed: primaryColor,
);

final textTheme = GoogleFonts.pressStart2pTextTheme(
themeData.textTheme,
);

final toolTipTheme = nesTooltipTheme ??
NesTooltipTheme(
background: textTheme.bodyMedium?.color ?? Colors.black,
textColor: themeData.colorScheme.background,
);

return themeData.copyWith(
textTheme: textTheme,
extensions: [
nesTheme,
nesButtonTheme,
iconTheme,
nesSelectionListTheme,
overlayTransitionTheme,
nesSnackbarTheme,
toolTipTheme,
...customExtensions,
],
);

final textTheme = GoogleFonts.pressStart2pTextTheme(
themeData.textTheme,
);

return themeData.copyWith(
textTheme: textTheme,
dividerTheme: DividerThemeData(
thickness: nesTheme.pixelSize.toDouble(),
color: textTheme.bodyMedium?.color,
Expand Down
223 changes: 223 additions & 0 deletions lib/src/widgets/nes_tooltip.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
import 'package:nes_ui/nes_ui.dart';

/// Enum with the possible placements for the arrow of the tooltip.
enum NesTooltipArrowPlacement {
/// The arrow will be placed on the left side of the tooltip.
left,

/// The arrow will be placed on the middle of the tooltip.
middle,

/// The arrow will be placed on the right side of the tooltip.
right,
}

/// {@template nes_tooltip}
/// A tooltip that appears when the user long-presses on a widget,
/// or hovers over it with a mouse.
/// {@endtemplate}
class NesTooltip extends StatefulWidget {
/// {@macro nes_tooltip}
const NesTooltip({
super.key,
required this.child,
required this.message,
this.arrowPlacement = NesTooltipArrowPlacement.middle,
});

/// The Widget that will trigger the tooltip.
final Widget child;

/// The message to be displayed in the tooltip.
final String message;

/// The placement of the arrow of the tooltip.
final NesTooltipArrowPlacement arrowPlacement;

@override
State<NesTooltip> createState() => _NesTooltipState();
}

class _NesTooltipState extends State<NesTooltip> {
var _show = false;

@override
Widget build(BuildContext context) {
final textStyle =
Theme.of(context).textTheme.labelMedium ?? const TextStyle();
final tooltipTheme = context.nesThemeExtension<NesTooltipTheme>();
final nesTheme = context.nesThemeExtension<NesTheme>();

return CustomPaint(
painter: _show
? _TooltipPainter(
color: tooltipTheme.background,
pixelSize: nesTheme.pixelSize.toDouble(),
arrowPlacement: widget.arrowPlacement,
textStyle: textStyle,
message: widget.message,
textColor: tooltipTheme.textColor,
)
: null,
child: GestureDetector(
onLongPress: () {
setState(() {
_show = true;
});
},
onLongPressEnd: (_) {
setState(() {
_show = false;
});
},
onLongPressCancel: () {
setState(() {
_show = false;
});
},
child: MouseRegion(
onEnter: (_) {
setState(() {
_show = true;
});
},
onExit: (_) {
setState(() {
_show = false;
});
},
child: widget.child,
),
),
);
}
}

class _TooltipPainter extends CustomPainter {
_TooltipPainter({
required this.color,
required this.pixelSize,
required this.arrowPlacement,
required this.textStyle,
required this.textColor,
required this.message,
});

final Color color;
final double pixelSize;
final NesTooltipArrowPlacement arrowPlacement;
final TextStyle textStyle;
final Color textColor;
final String message;

@override
SemanticsBuilderCallback? get semanticsBuilder => (size) {
return [
CustomPainterSemantics(
rect: Rect.fromLTWH(0, 0, size.width, size.height),
properties: SemanticsProperties(
label: message,
textDirection: TextDirection.ltr,
),
),
];
};

@override
void paint(Canvas canvas, Size childSize) {
final textPainter = TextPainter(
textDirection: TextDirection.ltr,
text: TextSpan(
text: message,
style: textStyle.copyWith(
color: textColor,
),
),
)..layout();

final textSize = textPainter.size;

final paint = Paint()..color = color;

final size = Size(
textSize.width + pixelSize * 4,
textSize.height + pixelSize * 2,
);

final arrowOffset = switch (arrowPlacement) {
NesTooltipArrowPlacement.left => pixelSize * 2,
NesTooltipArrowPlacement.middle => size.width / 2 - pixelSize,
NesTooltipArrowPlacement.right => size.width - pixelSize * 4,
};

canvas.save();

final translateX = switch (arrowPlacement) {
NesTooltipArrowPlacement.left => 0.0,
NesTooltipArrowPlacement.middle => -size.width / 2 + childSize.width / 2,
NesTooltipArrowPlacement.right => -size.width + childSize.width,
};

final translateY = -size.height - pixelSize * 4;

canvas
..translate(translateX, translateY)
..drawRect(
Rect.fromLTWH(0, 0, size.width, size.height),
paint,
)
..drawRect(
Rect.fromLTWH(
pixelSize,
-pixelSize,
size.width - pixelSize * 2,
pixelSize,
),
paint,
)
..drawRect(
Rect.fromLTWH(
pixelSize,
size.height,
size.width - pixelSize * 2,
pixelSize,
),
paint,
);

textPainter.paint(canvas, Offset(pixelSize * 2, pixelSize));

// Arrow
canvas
..drawRect(
Rect.fromLTWH(
arrowOffset,
size.height + pixelSize,
pixelSize * 2,
pixelSize,
),
paint,
)
..drawRect(
Rect.fromLTWH(
arrowOffset + pixelSize / 2,
size.height + pixelSize * 2,
pixelSize,
pixelSize,
),
paint,
)
..restore();
}

@override
bool shouldRepaint(_TooltipPainter oldDelegate) =>
oldDelegate.color != color ||
oldDelegate.pixelSize != pixelSize ||
oldDelegate.arrowPlacement != arrowPlacement ||
oldDelegate.textColor != textColor ||
oldDelegate.message != message ||
oldDelegate.textStyle != textStyle;
}
Loading

0 comments on commit b7c8e1b

Please sign in to comment.