Skip to content

Commit c50450d

Browse files
committed
feat: ✨ Tooltip actions' horizontal placement
- Added `insideLeft` and `insideRight` values in `TooltipActionPosition` enum. - Removed redundant parameters in `ActionWidget`. - Added proper assertions in `Showcase`. - Added `mainAxisSize` in `TooltipActionConfig`.
1 parent e243f42 commit c50450d

File tree

12 files changed

+260
-94
lines changed

12 files changed

+260
-94
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
Fixed example app to run in flutter version `v3.32.5`.
4444
- Fixed [#564](https://github.com/SimformSolutionsPvtLtd/flutter_showcaseview/issues/564) -
4545
Apply textScaler to tooltip widgets.
46+
- Improvement [#570](https://github.com/SimformSolutionsPvtLtd/flutter_showcaseview/pull/570) -
47+
Enable tooltip actions to be placed horizontally inside the tooltip.
4648

4749
## [4.0.1]
4850
- Fixed [#493](https://github.com/SimformSolutionsPvtLtd/flutter_showcaseview/issues/493) - ShowCase.withWidget not showing issue

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ _**For live web demo, visit [ShowcaseView Web Example](https://simformsolutionsp
1818
## Features
1919

2020
- Guide users through your app by highlighting specific widgets step by step.
21-
- Customize tooltips with titles, descriptions, and styling.
21+
- Customize tooltips with titles, descriptions, actions, and styling.
2222
- Handles scrolling the widget into view for showcasing.
2323
- Support for custom tooltip widgets.
2424
- Animation and transition effects for tooltip.

doc/documentation.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ _**For live web demo, visit [ShowcaseView Web Example](https://simformsolutionsp
1111
## Features
1212

1313
- Guide user through your app by highlighting specific widget step by step.
14-
- Customize tooltips with titles, descriptions, and styling.
14+
- Customize tooltips with titles, descriptions, actions, and styling.
1515
- Handles scrolling the widget into view for showcasing.
1616
- Support for custom tooltip widgets.
1717
- Animation and transition effects for tooltip.
@@ -77,6 +77,8 @@ The package offers extensive customization options for:
7777
- Animation effects and durations.
7878
- Interactive controls.
7979
- Auto-scrolling behavior.
80+
- Tooltip action buttons (previous, next, skip) with customizable styling and positioning.
81+
- Floating action widgets for additional interactive elements during showcases.
8082

8183
# Installation
8284

example/lib/main.dart

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -395,28 +395,19 @@ class _MailPageState extends State<MailPage> {
395395
description: 'Click here to compose mail',
396396
targetBorderRadius: const BorderRadius.all(Radius.circular(16)),
397397
showArrow: false,
398+
tooltipActionConfig: const TooltipActionConfig(
399+
position: TooltipActionPosition.insideRight,
400+
gapBetweenContentAndAction: 8,
401+
),
398402
tooltipActions: [
399-
TooltipActionButton(
400-
type: TooltipDefaultActionType.previous,
401-
name: 'Back',
403+
TooltipActionButton.custom(
404+
button: GestureDetector(
402405
onTap: () {
403-
// Write your code on button tap
404-
ShowcaseView.get().previous();
406+
ShowcaseView.get().dismiss();
405407
},
406-
backgroundColor: Colors.pink.shade50,
407-
textStyle: const TextStyle(
408-
color: Colors.pink,
409-
)),
410-
const TooltipActionButton(
411-
type: TooltipDefaultActionType.skip,
412-
name: 'Close',
413-
textStyle: TextStyle(
414-
color: Colors.white,
415-
),
416-
tailIcon: ActionButtonIcon(
417-
icon: Icon(
408+
child: const Icon(
418409
Icons.close,
419-
color: Colors.white,
410+
color: Colors.black,
420411
size: 15,
421412
),
422413
),

lib/src/models/tooltip_action_config.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class TooltipActionConfig {
3434
this.position = TooltipActionPosition.inside,
3535
this.gapBetweenContentAndAction = 10,
3636
this.crossAxisAlignment = CrossAxisAlignment.start,
37+
this.mainAxisSize = MainAxisSize.max,
3738
this.textBaseline,
3839
}) : assert(
3940
crossAxisAlignment != CrossAxisAlignment.stretch,
@@ -60,6 +61,9 @@ class TooltipActionConfig {
6061
/// This must be set if using baseline alignment.
6162
final TextBaseline? textBaseline;
6263

64+
/// Defines the main axis size of action buttons of tooltip action widget.
65+
final MainAxisSize mainAxisSize;
66+
6367
@override
6468
bool operator ==(Object other) {
6569
if (identical(this, other)) return true;
@@ -69,7 +73,8 @@ class TooltipActionConfig {
6973
position == other.position &&
7074
gapBetweenContentAndAction == other.gapBetweenContentAndAction &&
7175
crossAxisAlignment == other.crossAxisAlignment &&
72-
textBaseline == other.textBaseline;
76+
textBaseline == other.textBaseline &&
77+
mainAxisSize == other.mainAxisSize;
7378
}
7479

7580
@override
@@ -80,5 +85,6 @@ class TooltipActionConfig {
8085
gapBetweenContentAndAction,
8186
crossAxisAlignment,
8287
textBaseline,
88+
mainAxisSize,
8389
]);
8490
}

lib/src/showcase/showcase.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ class Showcase extends StatefulWidget {
6060
/// ```
6161
const Showcase({
6262
required GlobalKey key,
63-
required this.description,
6463
required this.child,
6564
this.title,
65+
this.description,
6666
this.titleTextAlign = TextAlign.start,
6767
this.descriptionTextAlign = TextAlign.start,
6868
this.titleAlignment = Alignment.center,
@@ -116,6 +116,11 @@ class Showcase extends StatefulWidget {
116116
width = null,
117117
container = null,
118118
showcaseKey = key,
119+
assert(
120+
title != null || description != null,
121+
"title and description both can't be null. If you don't want to "
122+
"provide those then use Showcase.withWidget() constructor",
123+
),
119124
assert(
120125
targetTooltipGap >= 0,
121126
'targetTooltipGap must be greater than 0',
@@ -225,6 +230,11 @@ class Showcase extends StatefulWidget {
225230
titleTextDirection = null,
226231
descriptionTextDirection = null,
227232
showcaseKey = key,
233+
assert(
234+
container != null,
235+
'A container widget must be provided with this constructor. If '
236+
'default showcase is desired then use Showcase() constructor',
237+
),
228238
assert(
229239
targetTooltipGap >= 0,
230240
'targetTooltipGap must be greater than 0',

lib/src/showcase/showcase_controller.dart

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ class ShowcaseController {
307307
disableDefaultChildGestures: config.disableDefaultTargetGestures,
308308
targetPadding: config.targetPadding,
309309
),
310-
ToolTipWidget(
310+
ToolTipWrapper(
311311
key: ValueKey(id),
312312
title: config.title,
313313
titleTextAlign: config.titleTextAlign,
@@ -398,18 +398,25 @@ class ShowcaseController {
398398
? config.tooltipActions!
399399
: showcaseView.globalTooltipActions ?? [];
400400
final actionDataLength = actionData.length;
401-
final lastAction = actionData.lastOrNull;
402-
final actionGap = _getTooltipActionConfig().actionGap;
401+
final actionConfig = _getTooltipActionConfig();
402+
final actionGap = actionConfig.actionGap;
403+
final areActionsInsideHorizontal = actionConfig.position.isInsideHorizontal;
403404

404405
return [
405406
for (var i = 0; i < actionDataLength; i++)
406407
if (doesHaveLocalActions ||
407408
!actionData[i]
408409
.hideActionWidgetForShowcase
409410
.contains(config.showcaseKey))
411+
// TODO: Switch to using spacing in [ActionWidget].
410412
Padding(
411413
padding: EdgeInsetsDirectional.only(
412-
end: actionData[i] == lastAction ? 0 : actionGap,
414+
end: i == (actionDataLength - 1) || areActionsInsideHorizontal
415+
? 0
416+
: actionGap,
417+
bottom: i == (actionDataLength - 1) || !areActionsInsideHorizontal
418+
? 0
419+
: actionGap,
413420
),
414421
child: TooltipActionButtonWidget(
415422
config: actionData[i],

lib/src/tooltip/tooltip.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import '../showcase/showcase_controller.dart';
66
import '../utils/constants.dart';
77
import '../utils/enum.dart';
88
import '../widget/action_widget.dart';
9-
import '../widget/default_tooltip_text_widget.dart';
109
import 'render_object_manager.dart';
10+
import 'tooltip_content.dart';
1111

1212
part 'animated_tooltip_multi_layout.dart';
1313
part 'arrow_painter.dart';
1414
part 'render_animation_delegate.dart';
1515
part 'render_position_delegate.dart';
1616
part 'tooltip_layout_id.dart';
17-
part 'tooltip_widget.dart';
17+
part 'tooltip_wrapper.dart';
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import 'package:flutter/material.dart';
2+
3+
import '../models/tooltip_action_config.dart';
4+
import '../widget/action_widget.dart';
5+
import '../widget/default_tooltip_text_widget.dart';
6+
7+
class ToolTipContent extends StatelessWidget {
8+
/// Builds the tooltip content layout based on action position configuration.
9+
///
10+
/// Supports following layouts:
11+
/// - Vertical layout (actions at bottom) for [TooltipActionPosition.inside]
12+
/// - Horizontal layout (actions on left/right) for
13+
/// [TooltipActionPosition.insideLeft] and [TooltipActionPosition.insideRight]
14+
const ToolTipContent({
15+
required this.description,
16+
required this.titleTextAlign,
17+
required this.descriptionTextAlign,
18+
required this.titleAlignment,
19+
required this.descriptionAlignment,
20+
required this.tooltipActionConfig,
21+
required this.tooltipActions,
22+
required this.textColor,
23+
this.title,
24+
this.titleTextStyle,
25+
this.descTextStyle,
26+
this.titlePadding,
27+
this.descriptionPadding,
28+
this.titleTextDirection,
29+
this.descriptionTextDirection,
30+
super.key,
31+
}) : assert(
32+
title != null || description != null,
33+
'Either title or description must be provided',
34+
);
35+
36+
final String? title;
37+
final TextAlign titleTextAlign;
38+
final String? description;
39+
final TextAlign descriptionTextAlign;
40+
final AlignmentGeometry titleAlignment;
41+
final AlignmentGeometry descriptionAlignment;
42+
final TextStyle? titleTextStyle;
43+
final TextStyle? descTextStyle;
44+
final Color textColor;
45+
final EdgeInsets? titlePadding;
46+
final EdgeInsets? descriptionPadding;
47+
final TextDirection? titleTextDirection;
48+
final TextDirection? descriptionTextDirection;
49+
final TooltipActionConfig tooltipActionConfig;
50+
final List<Widget> tooltipActions;
51+
52+
@override
53+
Widget build(BuildContext context) {
54+
final shouldShowActionsInside =
55+
tooltipActions.isNotEmpty && tooltipActionConfig.position.isInside;
56+
final textTheme = Theme.of(context).textTheme;
57+
58+
// Build title widget
59+
Widget? titleWidget;
60+
if (title case final title?) {
61+
titleWidget = DefaultTooltipTextWidget(
62+
padding: titlePadding ?? EdgeInsets.zero,
63+
text: title,
64+
textAlign: titleTextAlign,
65+
alignment: titleAlignment,
66+
textColor: textColor,
67+
textDirection: titleTextDirection,
68+
textStyle: titleTextStyle ??
69+
textTheme.titleLarge?.merge(TextStyle(color: textColor)),
70+
);
71+
}
72+
73+
// Build description widget
74+
Widget? descriptionWidget;
75+
if (description case final desc?) {
76+
descriptionWidget = DefaultTooltipTextWidget(
77+
padding: descriptionPadding ?? EdgeInsets.zero,
78+
text: desc,
79+
textAlign: descriptionTextAlign,
80+
alignment: descriptionAlignment,
81+
textColor: textColor,
82+
textDirection: descriptionTextDirection,
83+
textStyle: descTextStyle ??
84+
textTheme.titleSmall?.merge(TextStyle(color: textColor)),
85+
);
86+
}
87+
88+
// Build action widget
89+
Widget? actionWidget;
90+
if (shouldShowActionsInside) {
91+
actionWidget = ActionWidget(
92+
tooltipActionConfig: tooltipActionConfig,
93+
children: tooltipActions,
94+
);
95+
}
96+
97+
// For vertical action positioning (default), use Column layout
98+
final contentColumn = Column(
99+
mainAxisSize: MainAxisSize.min,
100+
children: <Widget>[
101+
if (titleWidget != null) titleWidget,
102+
if (descriptionWidget != null) descriptionWidget,
103+
if (actionWidget != null &&
104+
tooltipActionConfig.position.isInsideVertical)
105+
actionWidget,
106+
],
107+
);
108+
109+
// If no horizontal action positioning, return vertical layout
110+
if (actionWidget == null ||
111+
!tooltipActionConfig.position.isInsideHorizontal) {
112+
return contentColumn;
113+
}
114+
115+
// For horizontal action positioning, use Row layout
116+
final gap = SizedBox(
117+
width: tooltipActionConfig.gapBetweenContentAndAction,
118+
);
119+
return Row(
120+
mainAxisSize: MainAxisSize.min,
121+
children: [
122+
if (tooltipActionConfig.position.isInsideLeft) ...[
123+
actionWidget,
124+
gap,
125+
],
126+
Flexible(child: contentColumn),
127+
if (tooltipActionConfig.position.isInsideRight) ...[
128+
gap,
129+
actionWidget,
130+
],
131+
],
132+
);
133+
}
134+
}

0 commit comments

Comments
 (0)