diff --git a/playground/frontend/lib/constants/sizes.dart b/playground/frontend/lib/constants/sizes.dart index c73097f4cc21..1c7192570a40 100644 --- a/playground/frontend/lib/constants/sizes.dart +++ b/playground/frontend/lib/constants/sizes.dart @@ -54,6 +54,7 @@ const double kContainerHeight = 40.0; const double kCodeFontSize = 14.0; const double kLabelFontSize = 16.0; const double kHintFontSize = 16.0; +const double kTitleFontSize = 18.0; //divider size const double kDividerHeight = 1.0; diff --git a/playground/frontend/lib/modules/editor/components/run_button.dart b/playground/frontend/lib/modules/editor/components/run_button.dart index 8421ee8bed54..f29b014d933e 100644 --- a/playground/frontend/lib/modules/editor/components/run_button.dart +++ b/playground/frontend/lib/modules/editor/components/run_button.dart @@ -42,33 +42,37 @@ class RunButton extends StatelessWidget { @override Widget build(BuildContext context) { - return ShortcutTooltip( - shortcut: kRunShortcut, - child: ElevatedButton.icon( - icon: isRunning - ? SizedBox( - width: kIconSizeSm, - height: kIconSizeSm, - child: CircularProgressIndicator( - color: ThemeColors.of(context).primaryBackgroundTextColor, - ), - ) - : const Icon(Icons.play_arrow), - label: StreamBuilder( - stream: Provider.of(context).executionTime, - builder: (context, AsyncSnapshot state) { - final seconds = (state.data ?? 0) / kMsToSec; - final runText = AppLocalizations.of(context)!.run; - final cancelText = AppLocalizations.of(context)!.cancel; - final buttonText = isRunning ? cancelText : runText; - if (seconds > 0) { - return Text( - '$buttonText (${seconds.toStringAsFixed(kSecondsFractions)} s)', - ); - } - return Text(buttonText); - }), - onPressed: !isRunning ? runCode : cancelRun, + return SizedBox( + width: kRunButtonWidth, + height: kRunButtonHeight, + child: ShortcutTooltip( + shortcut: kRunShortcut, + child: ElevatedButton.icon( + icon: isRunning + ? SizedBox( + width: kIconSizeSm, + height: kIconSizeSm, + child: CircularProgressIndicator( + color: ThemeColors.of(context).primaryBackgroundTextColor, + ), + ) + : const Icon(Icons.play_arrow), + label: StreamBuilder( + stream: Provider.of(context).executionTime, + builder: (context, AsyncSnapshot state) { + final seconds = (state.data ?? 0) / kMsToSec; + final runText = AppLocalizations.of(context)!.run; + final cancelText = AppLocalizations.of(context)!.cancel; + final buttonText = isRunning ? cancelText : runText; + if (seconds > 0) { + return Text( + '$buttonText (${seconds.toStringAsFixed(kSecondsFractions)} s)', + ); + } + return Text(buttonText); + }), + onPressed: !isRunning ? runCode : cancelRun, + ), ), ); } diff --git a/playground/frontend/lib/modules/examples/components/description_popover/description_popover.dart b/playground/frontend/lib/modules/examples/components/description_popover/description_popover.dart new file mode 100644 index 000000000000..6a37c8ff4bb3 --- /dev/null +++ b/playground/frontend/lib/modules/examples/components/description_popover/description_popover.dart @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:playground/constants/font_weight.dart'; +import 'package:playground/constants/sizes.dart'; +import 'package:playground/modules/examples/models/example_model.dart'; + +const kDescriptionWidth = 300.0; + +class DescriptionPopover extends StatelessWidget { + final ExampleModel example; + + const DescriptionPopover({Key? key, required this.example}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: kDescriptionWidth, + child: Card( + child: Padding( + padding: const EdgeInsets.all(kLgSpacing), + child: Wrap( + runSpacing: kSmSpacing, + children: [ + Text( + example.name, + style: const TextStyle( + fontSize: kTitleFontSize, + fontWeight: kBoldWeight, + ), + ), + Text(example.description), + ], + ), + ), + ), + ); + } +} diff --git a/playground/frontend/lib/modules/examples/components/description_popover/description_popover_button.dart b/playground/frontend/lib/modules/examples/components/description_popover/description_popover_button.dart new file mode 100644 index 000000000000..d0eb6f7223e4 --- /dev/null +++ b/playground/frontend/lib/modules/examples/components/description_popover/description_popover_button.dart @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:aligned_dialog/aligned_dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:playground/config/theme.dart'; +import 'package:playground/constants/sizes.dart'; +import 'package:playground/modules/examples/components/description_popover/description_popover.dart'; +import 'package:playground/modules/examples/models/example_model.dart'; + +class DescriptionPopoverButton extends StatelessWidget { + final BuildContext? parentContext; + final ExampleModel example; + final Alignment followerAnchor; + final Alignment targetAnchor; + + const DescriptionPopoverButton({ + Key? key, + this.parentContext, + required this.example, + required this.followerAnchor, + required this.targetAnchor, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return IconButton( + iconSize: kIconSizeMd, + splashRadius: kIconButtonSplashRadius, + icon: Icon( + Icons.info_outline_rounded, + color: ThemeColors.of(context).grey1Color, + ), + onPressed: () { + _showDescriptionPopover( + parentContext ?? context, + example, + followerAnchor, + targetAnchor, + ); + }, + ); + } + + void _showDescriptionPopover( + BuildContext context, + ExampleModel example, + Alignment followerAnchor, + Alignment targetAnchor, + ) { + // close previous description dialog + Navigator.of(context, rootNavigator: true).popUntil((route) { + return route.isFirst; + }); + showAlignedDialog( + context: context, + builder: (dialogContext) => DescriptionPopover( + example: example, + ), + followerAnchor: followerAnchor, + targetAnchor: targetAnchor, + barrierColor: Colors.transparent, + ); + } +} diff --git a/playground/frontend/lib/modules/examples/components/example_list/expansion_panel_item.dart b/playground/frontend/lib/modules/examples/components/example_list/expansion_panel_item.dart index af37ac196e45..74a6e23fb53d 100644 --- a/playground/frontend/lib/modules/examples/components/example_list/expansion_panel_item.dart +++ b/playground/frontend/lib/modules/examples/components/example_list/expansion_panel_item.dart @@ -19,6 +19,7 @@ import 'package:flutter/material.dart'; import 'package:playground/constants/sizes.dart'; import 'package:playground/modules/analytics/analytics_service.dart'; +import 'package:playground/modules/examples/components/description_popover/description_popover_button.dart'; import 'package:playground/modules/examples/models/example_model.dart'; import 'package:playground/pages/playground/states/examples_state.dart'; import 'package:playground/pages/playground/states/playground_state.dart'; @@ -59,16 +60,26 @@ class ExpansionPanelItem extends StatelessWidget { color: Colors.transparent, margin: const EdgeInsets.only(left: kXxlSpacing), height: kContainerHeight, - child: Row( - children: [ - // Wrapped with Row for better user interaction and positioning - Text( - example.name, - style: example == selectedExample - ? const TextStyle(fontWeight: FontWeight.bold) - : const TextStyle(), - ), - ], + child: Padding( + padding: const EdgeInsets.only(right: kLgSpacing), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Wrapped with Row for better user interaction and positioning + Text( + example.name, + style: example == selectedExample + ? const TextStyle(fontWeight: FontWeight.bold) + : const TextStyle(), + ), + DescriptionPopoverButton( + parentContext: context, + example: example, + followerAnchor: Alignment.topLeft, + targetAnchor: Alignment.topRight, + ), + ], + ), ), ), ), diff --git a/playground/frontend/lib/modules/examples/example_selector.dart b/playground/frontend/lib/modules/examples/example_selector.dart index d2024fd0eb69..b3a4a6c23a86 100644 --- a/playground/frontend/lib/modules/examples/example_selector.dart +++ b/playground/frontend/lib/modules/examples/example_selector.dart @@ -124,7 +124,13 @@ class _ExampleSelectorState extends State builder: (context, exampleState, playgroundState, child) => Stack( children: [ GestureDetector( - onTap: () => closeDropdown(exampleState), + onTap: () { + closeDropdown(exampleState); + // handle description dialogs + Navigator.of(context, rootNavigator: true).popUntil((route) { + return route.isFirst; + }); + }, child: Container( color: Colors.transparent, height: double.infinity, diff --git a/playground/frontend/lib/pages/playground/components/editor_textarea_wrapper.dart b/playground/frontend/lib/pages/playground/components/editor_textarea_wrapper.dart index 8626d1c53587..5047d4c77022 100644 --- a/playground/frontend/lib/pages/playground/components/editor_textarea_wrapper.dart +++ b/playground/frontend/lib/pages/playground/components/editor_textarea_wrapper.dart @@ -23,6 +23,7 @@ import 'package:playground/modules/analytics/analytics_service.dart'; import 'package:playground/modules/editor/components/editor_textarea.dart'; import 'package:playground/modules/editor/components/pipeline_options_text_field.dart'; import 'package:playground/modules/editor/components/run_button.dart'; +import 'package:playground/modules/examples/components/description_popover/description_popover_button.dart'; import 'package:playground/modules/examples/models/example_model.dart'; import 'package:playground/modules/notifications/components/notification.dart'; import 'package:playground/modules/sdk/models/sdk.dart'; @@ -61,33 +62,42 @@ class CodeTextAreaWrapper extends StatelessWidget { Positioned( right: kXlSpacing, top: kXlSpacing, - width: kRunButtonWidth, height: kRunButtonHeight, - child: RunButton( - isRunning: state.isCodeRunning, - cancelRun: () { - state.cancelRun().catchError( - (_) => NotificationManager.showError( - context, - AppLocalizations.of(context)!.runCode, - AppLocalizations.of(context)!.cancelExecution, - ), - ); - }, - runCode: () { - final stopwatch = Stopwatch()..start(); - state.runCode( - onFinish: () { - AnalyticsService.get(context).trackRunTimeEvent( - state.selectedExample?.path ?? - '${AppLocalizations.of(context)!.unknownExample}, sdk ${state.sdk.displayName}', - stopwatch.elapsedMilliseconds, + child: Row( + children: [ + if (state.selectedExample != null) + DescriptionPopoverButton( + example: state.selectedExample!, + followerAnchor: Alignment.topRight, + targetAnchor: Alignment.bottomRight, + ), + RunButton( + isRunning: state.isCodeRunning, + cancelRun: () { + state.cancelRun().catchError( + (_) => NotificationManager.showError( + context, + AppLocalizations.of(context)!.runCode, + AppLocalizations.of(context)!.cancelExecution, + ), + ); + }, + runCode: () { + final stopwatch = Stopwatch()..start(); + state.runCode( + onFinish: () { + AnalyticsService.get(context).trackRunTimeEvent( + state.selectedExample?.path ?? + '${AppLocalizations.of(context)!.unknownExample}, sdk ${state.sdk.displayName}', + stopwatch.elapsedMilliseconds, + ); + }, ); + AnalyticsService.get(context) + .trackClickRunEvent(state.selectedExample); }, - ); - AnalyticsService.get(context) - .trackClickRunEvent(state.selectedExample); - }, + ), + ], ), ), ],