Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
Fixed example app to run in flutter version `v3.32.5`.
- Fixed [#564](https://github.com/SimformSolutionsPvtLtd/flutter_showcaseview/issues/564) -
Apply textScaler to tooltip widgets.
- Improvement [#570](https://github.com/SimformSolutionsPvtLtd/flutter_showcaseview/pull/570) -
Enable tooltip actions to be placed horizontally inside the tooltip.

## [4.0.1]
- Fixed [#493](https://github.com/SimformSolutionsPvtLtd/flutter_showcaseview/issues/493) - ShowCase.withWidget not showing issue
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ _**For live web demo, visit [ShowcaseView Web Example](https://simformsolutionsp
## Features

- Guide users through your app by highlighting specific widgets step by step.
- Customize tooltips with titles, descriptions, and styling.
- Customize tooltips with titles, descriptions, actions, and styling.
- Handles scrolling the widget into view for showcasing.
- Support for custom tooltip widgets.
- Animation and transition effects for tooltip.
Expand Down
4 changes: 3 additions & 1 deletion doc/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ _**For live web demo, visit [ShowcaseView Web Example](https://simformsolutionsp
## Features

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

# Installation

Expand Down
27 changes: 9 additions & 18 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -395,28 +395,19 @@ class _MailPageState extends State<MailPage> {
description: 'Click here to compose mail',
targetBorderRadius: const BorderRadius.all(Radius.circular(16)),
showArrow: false,
tooltipActionConfig: const TooltipActionConfig(
position: TooltipActionPosition.insideRight,
gapBetweenContentAndAction: 8,
),
tooltipActions: [
TooltipActionButton(
type: TooltipDefaultActionType.previous,
name: 'Back',
TooltipActionButton.custom(
button: GestureDetector(
onTap: () {
// Write your code on button tap
ShowcaseView.get().previous();
ShowcaseView.get().dismiss();
},
backgroundColor: Colors.pink.shade50,
textStyle: const TextStyle(
color: Colors.pink,
)),
const TooltipActionButton(
type: TooltipDefaultActionType.skip,
name: 'Close',
textStyle: TextStyle(
color: Colors.white,
),
tailIcon: ActionButtonIcon(
icon: Icon(
child: const Icon(
Icons.close,
color: Colors.white,
color: Colors.black,
size: 15,
),
),
Expand Down
8 changes: 7 additions & 1 deletion lib/src/models/tooltip_action_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class TooltipActionConfig {
this.position = TooltipActionPosition.inside,
this.gapBetweenContentAndAction = 10,
this.crossAxisAlignment = CrossAxisAlignment.start,
this.mainAxisSize = MainAxisSize.max,
this.textBaseline,
}) : assert(
crossAxisAlignment != CrossAxisAlignment.stretch,
Expand All @@ -60,6 +61,9 @@ class TooltipActionConfig {
/// This must be set if using baseline alignment.
final TextBaseline? textBaseline;

/// Defines the main axis size of action buttons of tooltip action widget.
final MainAxisSize mainAxisSize;

@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
Expand All @@ -69,7 +73,8 @@ class TooltipActionConfig {
position == other.position &&
gapBetweenContentAndAction == other.gapBetweenContentAndAction &&
crossAxisAlignment == other.crossAxisAlignment &&
textBaseline == other.textBaseline;
textBaseline == other.textBaseline &&
mainAxisSize == other.mainAxisSize;
}

@override
Expand All @@ -80,5 +85,6 @@ class TooltipActionConfig {
gapBetweenContentAndAction,
crossAxisAlignment,
textBaseline,
mainAxisSize,
]);
}
12 changes: 11 additions & 1 deletion lib/src/showcase/showcase.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ class Showcase extends StatefulWidget {
/// ```
const Showcase({
required GlobalKey key,
required this.description,
required this.child,
this.title,
this.description,
Copy link
Preview

Copilot AI Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description parameter should remain required to maintain backward compatibility. Making it optional without a major version bump could break existing implementations that rely on the required nature of this parameter.

Suggested change
this.description,
required this.description,

Copilot uses AI. Check for mistakes.

this.titleTextAlign = TextAlign.start,
this.descriptionTextAlign = TextAlign.start,
this.titleAlignment = Alignment.center,
Expand Down Expand Up @@ -116,6 +116,11 @@ class Showcase extends StatefulWidget {
width = null,
container = null,
showcaseKey = key,
assert(
title != null || description != null,
"title and description both can't be null. If you don't want to "
"provide those then use Showcase.withWidget() constructor",
),
assert(
targetTooltipGap >= 0,
'targetTooltipGap must be greater than 0',
Expand Down Expand Up @@ -225,6 +230,11 @@ class Showcase extends StatefulWidget {
titleTextDirection = null,
descriptionTextDirection = null,
showcaseKey = key,
assert(
container != null,
'A container widget must be provided with this constructor. If '
'default showcase is desired then use Showcase() constructor',
),
assert(
targetTooltipGap >= 0,
'targetTooltipGap must be greater than 0',
Expand Down
15 changes: 11 additions & 4 deletions lib/src/showcase/showcase_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ class ShowcaseController {
disableDefaultChildGestures: config.disableDefaultTargetGestures,
targetPadding: config.targetPadding,
),
ToolTipWidget(
ToolTipWrapper(
key: ValueKey(id),
title: config.title,
titleTextAlign: config.titleTextAlign,
Expand Down Expand Up @@ -398,18 +398,25 @@ class ShowcaseController {
? config.tooltipActions!
: showcaseView.globalTooltipActions ?? [];
final actionDataLength = actionData.length;
final lastAction = actionData.lastOrNull;
final actionGap = _getTooltipActionConfig().actionGap;
final actionConfig = _getTooltipActionConfig();
final actionGap = actionConfig.actionGap;
final areActionsInsideHorizontal = actionConfig.position.isInsideHorizontal;

return [
for (var i = 0; i < actionDataLength; i++)
if (doesHaveLocalActions ||
!actionData[i]
.hideActionWidgetForShowcase
.contains(config.showcaseKey))
// TODO: Switch to using spacing in [ActionWidget].
Padding(
padding: EdgeInsetsDirectional.only(
end: actionData[i] == lastAction ? 0 : actionGap,
end: i == (actionDataLength - 1) || areActionsInsideHorizontal
? 0
: actionGap,
bottom: i == (actionDataLength - 1) || !areActionsInsideHorizontal
? 0
: actionGap,
),
child: TooltipActionButtonWidget(
config: actionData[i],
Expand Down
4 changes: 2 additions & 2 deletions lib/src/tooltip/tooltip.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import '../showcase/showcase_controller.dart';
import '../utils/constants.dart';
import '../utils/enum.dart';
import '../widget/action_widget.dart';
import '../widget/default_tooltip_text_widget.dart';
import 'render_object_manager.dart';
import 'tooltip_content.dart';

part 'animated_tooltip_multi_layout.dart';
part 'arrow_painter.dart';
part 'render_animation_delegate.dart';
part 'render_position_delegate.dart';
part 'tooltip_layout_id.dart';
part 'tooltip_widget.dart';
part 'tooltip_wrapper.dart';
134 changes: 134 additions & 0 deletions lib/src/tooltip/tooltip_content.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import 'package:flutter/material.dart';

import '../models/tooltip_action_config.dart';
import '../widget/action_widget.dart';
import '../widget/default_tooltip_text_widget.dart';

class ToolTipContent extends StatelessWidget {
/// Builds the tooltip content layout based on action position configuration.
///
/// Supports following layouts:
/// - Vertical layout (actions at bottom) for [TooltipActionPosition.inside]
/// - Horizontal layout (actions on left/right) for
/// [TooltipActionPosition.insideLeft] and [TooltipActionPosition.insideRight]
const ToolTipContent({
required this.description,
required this.titleTextAlign,
required this.descriptionTextAlign,
required this.titleAlignment,
required this.descriptionAlignment,
required this.tooltipActionConfig,
required this.tooltipActions,
required this.textColor,
this.title,
this.titleTextStyle,
this.descTextStyle,
this.titlePadding,
this.descriptionPadding,
this.titleTextDirection,
this.descriptionTextDirection,
super.key,
}) : assert(
title != null || description != null,
'Either title or description must be provided',
);

final String? title;
final TextAlign titleTextAlign;
final String? description;
final TextAlign descriptionTextAlign;
final AlignmentGeometry titleAlignment;
final AlignmentGeometry descriptionAlignment;
final TextStyle? titleTextStyle;
final TextStyle? descTextStyle;
final Color textColor;
final EdgeInsets? titlePadding;
final EdgeInsets? descriptionPadding;
final TextDirection? titleTextDirection;
final TextDirection? descriptionTextDirection;
final TooltipActionConfig tooltipActionConfig;
final List<Widget> tooltipActions;

@override
Widget build(BuildContext context) {
final shouldShowActionsInside =
tooltipActions.isNotEmpty && tooltipActionConfig.position.isInside;
final textTheme = Theme.of(context).textTheme;

// Build title widget
Widget? titleWidget;
if (title case final title?) {
titleWidget = DefaultTooltipTextWidget(
padding: titlePadding ?? EdgeInsets.zero,
text: title,
textAlign: titleTextAlign,
alignment: titleAlignment,
textColor: textColor,
textDirection: titleTextDirection,
textStyle: titleTextStyle ??
textTheme.titleLarge?.merge(TextStyle(color: textColor)),
);
}

// Build description widget
Widget? descriptionWidget;
if (description case final desc?) {
descriptionWidget = DefaultTooltipTextWidget(
padding: descriptionPadding ?? EdgeInsets.zero,
text: desc,
textAlign: descriptionTextAlign,
alignment: descriptionAlignment,
textColor: textColor,
textDirection: descriptionTextDirection,
textStyle: descTextStyle ??
textTheme.titleSmall?.merge(TextStyle(color: textColor)),
);
}

// Build action widget
Widget? actionWidget;
if (shouldShowActionsInside) {
actionWidget = ActionWidget(
tooltipActionConfig: tooltipActionConfig,
children: tooltipActions,
);
}

// For vertical action positioning (default), use Column layout
final contentColumn = Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (titleWidget != null) titleWidget,
if (descriptionWidget != null) descriptionWidget,
if (actionWidget != null &&
tooltipActionConfig.position.isInsideVertical)
actionWidget,
],
);

// If no horizontal action positioning, return vertical layout
if (actionWidget == null ||
!tooltipActionConfig.position.isInsideHorizontal) {
return contentColumn;
}

// For horizontal action positioning, use Row layout
final gap = SizedBox(
width: tooltipActionConfig.gapBetweenContentAndAction,
);
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (tooltipActionConfig.position.isInsideLeft) ...[
actionWidget,
gap,
],
Flexible(child: contentColumn),
if (tooltipActionConfig.position.isInsideRight) ...[
gap,
actionWidget,
],
],
);
}
}
Loading