فهرست منبع

Merge branch 'infra_ui/overlay_base' into main

Jaylen Bian 3 سال پیش
والد
کامیت
1d7540c954

+ 154 - 108
app_flowy/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart

@@ -1,3 +1,4 @@
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
 import 'package:flutter/material.dart';
 import 'package:provider/provider.dart';
@@ -48,131 +49,176 @@ class OverlayScreen extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return Scaffold(
-        appBar: AppBar(
-          title: const Text('Overlay Demo'),
-        ),
-        body: ChangeNotifierProvider(
-          create: (context) => OverlayDemoConfiguration(AnchorDirection.rightWithTopAligned, OverlapBehaviour.stretch),
-          child: Builder(builder: (providerContext) {
-            return Center(
-              child: ConstrainedBox(
-                constraints: const BoxConstraints.tightFor(width: 500),
-                child: Column(
-                  crossAxisAlignment: CrossAxisAlignment.center,
-                  children: [
-                    const SizedBox(height: 48.0),
-                    ElevatedButton(
-                      onPressed: () {
-                        final windowSize = MediaQuery.of(context).size;
-                        FlowyOverlay.of(context).insertCustom(
-                          widget: Positioned(
-                            left: windowSize.width / 2.0 - 100,
-                            top: 200,
-                            child: SizedBox(
-                              width: 200,
-                              height: 100,
+      appBar: AppBar(
+        title: const Text('Overlay Demo'),
+      ),
+      body: ChangeNotifierProvider(
+        create: (context) => OverlayDemoConfiguration(AnchorDirection.rightWithTopAligned, OverlapBehaviour.stretch),
+        child: Builder(builder: (providerContext) {
+          return Center(
+            child: ConstrainedBox(
+              constraints: const BoxConstraints.tightFor(width: 500),
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.center,
+                children: [
+                  const SizedBox(height: 48.0),
+                  ElevatedButton(
+                    onPressed: () {
+                      final windowSize = MediaQuery.of(context).size;
+                      FlowyOverlay.of(context).insertCustom(
+                        widget: Positioned(
+                          left: windowSize.width / 2.0 - 100,
+                          top: 200,
+                          child: SizedBox(
+                            width: 200,
+                            height: 100,
+                            child: Card(
+                              color: Colors.green[200],
+                              child: GestureDetector(
+                                // ignore: avoid_print
+                                onTapDown: (_) => print('Hello Flutter'),
+                                child: const Center(child: FlutterLogo(size: 100)),
+                              ),
+                            ),
+                          ),
+                        ),
+                        identifier: 'overlay_flutter_logo',
+                        delegate: null,
+                      );
+                    },
+                    child: const Text('Show Overlay'),
+                  ),
+                  const SizedBox(height: 24.0),
+                  DropdownButton<AnchorDirection>(
+                    value: providerContext.watch<OverlayDemoConfiguration>().anchorDirection,
+                    onChanged: (AnchorDirection? newValue) {
+                      if (newValue != null) {
+                        providerContext.read<OverlayDemoConfiguration>().anchorDirection = newValue;
+                      }
+                    },
+                    items: AnchorDirection.values.map((AnchorDirection classType) {
+                      return DropdownMenuItem<AnchorDirection>(value: classType, child: Text(classType.toString()));
+                    }).toList(),
+                  ),
+                  const SizedBox(height: 24.0),
+                  DropdownButton<OverlapBehaviour>(
+                    value: providerContext.watch<OverlayDemoConfiguration>().overlapBehaviour,
+                    onChanged: (OverlapBehaviour? newValue) {
+                      if (newValue != null) {
+                        providerContext.read<OverlayDemoConfiguration>().overlapBehaviour = newValue;
+                      }
+                    },
+                    items: OverlapBehaviour.values.map((OverlapBehaviour classType) {
+                      return DropdownMenuItem<OverlapBehaviour>(value: classType, child: Text(classType.toString()));
+                    }).toList(),
+                  ),
+                  const SizedBox(height: 24.0),
+                  Builder(builder: (buttonContext) {
+                    return SizedBox(
+                      height: 100,
+                      child: ElevatedButton(
+                        onPressed: () {
+                          FlowyOverlay.of(context).insertWithAnchor(
+                            widget: SizedBox(
+                              width: 300,
+                              height: 50,
                               child: Card(
-                                color: Colors.green[200],
+                                color: Colors.grey[200],
                                 child: GestureDetector(
                                   // ignore: avoid_print
                                   onTapDown: (_) => print('Hello Flutter'),
-                                  child: const Center(child: FlutterLogo(size: 100)),
+                                  child: const Center(child: FlutterLogo(size: 50)),
                                 ),
                               ),
                             ),
+                            identifier: 'overlay_anchored_card',
+                            delegate: null,
+                            anchorContext: buttonContext,
+                            anchorDirection: providerContext.read<OverlayDemoConfiguration>().anchorDirection,
+                            overlapBehaviour: providerContext.read<OverlayDemoConfiguration>().overlapBehaviour,
+                          );
+                        },
+                        child: const Text('Show Anchored Overlay'),
+                      ),
+                    );
+                  }),
+                  const SizedBox(height: 24.0),
+                  ElevatedButton(
+                    onPressed: () {
+                      final windowSize = MediaQuery.of(context).size;
+                      FlowyOverlay.of(context).insertWithRect(
+                        widget: SizedBox(
+                          width: 200,
+                          height: 100,
+                          child: Card(
+                            color: Colors.orange[200],
+                            child: GestureDetector(
+                              // ignore: avoid_print
+                              onTapDown: (_) => print('Hello Flutter'),
+                              child: const Center(child: FlutterLogo(size: 100)),
+                            ),
                           ),
-                          identifier: 'overlay_flutter_logo',
-                          delegate: null,
-                        );
-                      },
-                      child: const Text('Show Overlay'),
-                    ),
-                    const SizedBox(height: 24.0),
-                    DropdownButton<AnchorDirection>(
-                      value: providerContext.watch<OverlayDemoConfiguration>().anchorDirection,
-                      onChanged: (AnchorDirection? newValue) {
-                        if (newValue != null) {
-                          providerContext.read<OverlayDemoConfiguration>().anchorDirection = newValue;
-                        }
-                      },
-                      items: AnchorDirection.values.map((AnchorDirection classType) {
-                        return DropdownMenuItem<AnchorDirection>(value: classType, child: Text(classType.toString()));
-                      }).toList(),
-                    ),
-                    const SizedBox(height: 24.0),
-                    DropdownButton<OverlapBehaviour>(
-                      value: providerContext.watch<OverlayDemoConfiguration>().overlapBehaviour,
-                      onChanged: (OverlapBehaviour? newValue) {
-                        if (newValue != null) {
-                          providerContext.read<OverlayDemoConfiguration>().overlapBehaviour = newValue;
-                        }
-                      },
-                      items: OverlapBehaviour.values.map((OverlapBehaviour classType) {
-                        return DropdownMenuItem<OverlapBehaviour>(value: classType, child: Text(classType.toString()));
-                      }).toList(),
-                    ),
-                    const SizedBox(height: 24.0),
-                    Builder(builder: (buttonContext) {
-                      return SizedBox(
-                        height: 100,
-                        child: ElevatedButton(
-                          onPressed: () {
-                            FlowyOverlay.of(context).insertWithAnchor(
-                              widget: SizedBox(
-                                width: 300,
-                                height: 50,
-                                child: Card(
-                                  color: Colors.grey[200],
-                                  child: GestureDetector(
-                                    // ignore: avoid_print
-                                    onTapDown: (_) => print('Hello Flutter'),
-                                    child: const Center(child: FlutterLogo(size: 50)),
-                                  ),
-                                ),
-                              ),
-                              identifier: 'overlay_anchored_card',
-                              delegate: null,
-                              anchorContext: buttonContext,
-                              anchorDirection: providerContext.read<OverlayDemoConfiguration>().anchorDirection,
-                              overlapBehaviour: providerContext.read<OverlayDemoConfiguration>().overlapBehaviour,
-                            );
-                          },
-                          child: const Text('Show Anchored Overlay'),
                         ),
+                        identifier: 'overlay_positioned_card',
+                        delegate: null,
+                        anchorPosition: Offset(0, windowSize.height - 200),
+                        anchorSize: Size.zero,
+                        anchorDirection: providerContext.read<OverlayDemoConfiguration>().anchorDirection,
+                        overlapBehaviour: providerContext.read<OverlayDemoConfiguration>().overlapBehaviour,
                       );
-                    }),
-                    const SizedBox(height: 24.0),
-                    ElevatedButton(
+                    },
+                    child: const Text('Show Positioned Overlay'),
+                  ),
+                  const SizedBox(height: 24.0),
+                  Builder(builder: (buttonContext) {
+                    return ElevatedButton(
                       onPressed: () {
-                        final windowSize = MediaQuery.of(context).size;
-                        FlowyOverlay.of(context).insertWithRect(
-                          widget: SizedBox(
-                            width: 200,
-                            height: 100,
-                            child: Card(
-                              color: Colors.orange[200],
-                              child: GestureDetector(
-                                // ignore: avoid_print
-                                onTapDown: (_) => print('Hello Flutter'),
-                                child: const Center(child: FlutterLogo(size: 100)),
-                              ),
+                        ListOverlay.showWithAnchor(
+                          context,
+                          itemBuilder: (_, index) => Card(
+                            margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0),
+                            elevation: 0,
+                            child: Text(
+                              'Option $index',
+                              style: const TextStyle(fontSize: 20.0, color: Colors.black),
                             ),
                           ),
-                          identifier: 'overlay_positioned_card',
-                          delegate: null,
-                          anchorPosition: Offset(0, windowSize.height - 200),
-                          anchorSize: Size.zero,
+                          itemCount: 10,
+                          identifier: 'overlay_list_menu',
+                          anchorContext: buttonContext,
                           anchorDirection: providerContext.read<OverlayDemoConfiguration>().anchorDirection,
                           overlapBehaviour: providerContext.read<OverlayDemoConfiguration>().overlapBehaviour,
+                          maxWidth: 200.0,
+                          maxHeight: 200.0,
                         );
                       },
-                      child: const Text('Show Positioned Overlay'),
-                    ),
-                  ],
-                ),
+                      child: const Text('Show List Overlay'),
+                    );
+                  }),
+                  const SizedBox(height: 24.0),
+                  Builder(builder: (buttonContext) {
+                    return ElevatedButton(
+                      onPressed: () {
+                        OptionOverlay.showWithAnchor(
+                          context,
+                          items: <String>['Alpha', 'Beta', 'Charlie', 'Delta', 'Echo', 'Foxtrot', 'Golf', 'Hotel'],
+                          onHover: (value, index) => print('Did hover option $index, value $value'),
+                          onTap: (value, index) => print('Did tap option $index, value $value'),
+                          identifier: 'overlay_options',
+                          anchorContext: buttonContext,
+                          anchorDirection: providerContext.read<OverlayDemoConfiguration>().anchorDirection,
+                          overlapBehaviour: providerContext.read<OverlayDemoConfiguration>().overlapBehaviour,
+                        );
+                      },
+                      child: const Text('Show Options Overlay'),
+                    );
+                  }),
+                ],
               ),
-            );
-          }),
-        ));
+            ),
+          );
+        }),
+      ),
+    );
   }
 }

+ 3 - 0
app_flowy/packages/flowy_infra_ui/lib/basis.dart

@@ -3,3 +3,6 @@ import 'package:flutter/material.dart';
 // MARK: - Shared Builder
 
 typedef WidgetBuilder = Widget Function();
+
+typedef IndexedCallback = void Function(int index);
+typedef IndexedValueCallback<T> = void Function(T value, int index);

+ 2 - 0
app_flowy/packages/flowy_infra_ui/lib/flowy_infra_ui.dart

@@ -6,3 +6,5 @@ export 'src/keyboard/keyboard_visibility_detector.dart';
 
 // Overlay
 export 'src/flowy_overlay/flowy_overlay.dart';
+export 'src/flowy_overlay/list_overlay.dart';
+export 'src/flowy_overlay/option_overlay.dart';

+ 2 - 0
app_flowy/packages/flowy_infra_ui/lib/flowy_infra_ui_web.dart

@@ -6,3 +6,5 @@ export 'src/keyboard/keyboard_visibility_detector.dart';
 
 // Overlay
 export 'src/flowy_overlay/flowy_overlay.dart';
+export 'src/flowy_overlay/list_overlay.dart';
+export 'src/flowy_overlay/option_overlay.dart';

+ 99 - 0
app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart

@@ -0,0 +1,99 @@
+import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
+import 'package:flutter/material.dart';
+
+class ListOverlay extends StatelessWidget {
+  const ListOverlay({
+    Key? key,
+    required this.itemBuilder,
+    this.itemCount,
+    this.controller,
+    this.maxWidth = double.infinity,
+    this.maxHeight = double.infinity,
+  }) : super(key: key);
+
+  final IndexedWidgetBuilder itemBuilder;
+  final int? itemCount;
+  final ScrollController? controller;
+  final double maxWidth;
+  final double maxHeight;
+
+  static void showWithAnchor(
+    BuildContext context, {
+    required String identifier,
+    required IndexedWidgetBuilder itemBuilder,
+    int? itemCount,
+    ScrollController? controller,
+    double maxWidth = double.infinity,
+    double maxHeight = double.infinity,
+    required BuildContext anchorContext,
+    AnchorDirection? anchorDirection,
+    FlowyOverlayDelegate? delegate,
+    OverlapBehaviour? overlapBehaviour,
+  }) {
+    FlowyOverlay.of(context).insertWithAnchor(
+      widget: ListOverlay(
+        itemBuilder: itemBuilder,
+        itemCount: itemCount,
+        controller: controller,
+        maxWidth: maxWidth,
+        maxHeight: maxHeight,
+      ),
+      identifier: identifier,
+      anchorContext: anchorContext,
+      anchorDirection: anchorDirection,
+      delegate: delegate,
+      overlapBehaviour: overlapBehaviour,
+    );
+  }
+
+  static void showWithRect(
+    BuildContext context, {
+    required BuildContext anchorContext,
+    required String identifier,
+    required IndexedWidgetBuilder itemBuilder,
+    int? itemCount,
+    ScrollController? controller,
+    double maxWidth = double.infinity,
+    double maxHeight = double.infinity,
+    required Offset anchorPosition,
+    required Size anchorSize,
+    AnchorDirection? anchorDirection,
+    FlowyOverlayDelegate? delegate,
+    OverlapBehaviour? overlapBehaviour,
+  }) {
+    FlowyOverlay.of(context).insertWithRect(
+      widget: ListOverlay(
+        itemBuilder: itemBuilder,
+        itemCount: itemCount,
+        controller: controller,
+        maxWidth: maxWidth,
+        maxHeight: maxHeight,
+      ),
+      identifier: identifier,
+      anchorPosition: anchorPosition,
+      anchorSize: anchorSize,
+      anchorDirection: anchorDirection,
+      delegate: delegate,
+      overlapBehaviour: overlapBehaviour,
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      constraints: BoxConstraints.tight(Size(maxWidth, maxHeight)),
+      decoration: BoxDecoration(
+        color: Colors.white,
+        borderRadius: const BorderRadius.all(Radius.circular(6)),
+        boxShadow: [
+          BoxShadow(color: Colors.black.withOpacity(0.1), spreadRadius: 1, blurRadius: 20.0),
+        ],
+      ),
+      child: ListView.builder(
+        shrinkWrap: true,
+        itemBuilder: itemBuilder,
+        itemCount: itemCount,
+      ),
+    );
+  }
+}

+ 97 - 0
app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/option_overlay.dart

@@ -0,0 +1,97 @@
+import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
+import 'package:flutter/material.dart';
+
+class OptionItem {
+  const OptionItem(this.icon, this.title);
+
+  final Icon? icon;
+  final String title;
+}
+
+class OptionOverlay<T> extends StatelessWidget {
+  const OptionOverlay({
+    Key? key,
+    required this.items,
+    this.onHover,
+    this.onTap,
+  }) : super(key: key);
+
+  final List<T> items;
+  final IndexedValueCallback<T>? onHover;
+  final IndexedValueCallback<T>? onTap;
+
+  static void showWithAnchor<T>(
+    BuildContext context, {
+    required String identifier,
+    required List<T> items,
+    IndexedValueCallback<T>? onHover,
+    IndexedValueCallback<T>? onTap,
+    required BuildContext anchorContext,
+    AnchorDirection? anchorDirection,
+    FlowyOverlayDelegate? delegate,
+    OverlapBehaviour? overlapBehaviour,
+  }) {
+    FlowyOverlay.of(context).insertWithAnchor(
+      widget: OptionOverlay(
+        items: items,
+        onHover: onHover,
+        onTap: onTap,
+      ),
+      identifier: identifier,
+      anchorContext: anchorContext,
+      anchorDirection: anchorDirection,
+      delegate: delegate,
+      overlapBehaviour: overlapBehaviour,
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final List<_OptionListItem> listItems = items.map((e) => _OptionListItem(e)).toList();
+    return ListOverlay(
+      itemBuilder: (context, index) {
+        return MouseRegion(
+          cursor: SystemMouseCursors.click,
+          onHover: onHover != null ? (_) => onHover!(items[index], index) : null,
+          child: GestureDetector(
+            onTap: onTap != null ? () => onTap!(items[index], index) : null,
+            child: listItems[index],
+          ),
+        );
+      },
+      itemCount: listItems.length,
+    );
+  }
+}
+
+class _OptionListItem<T> extends StatelessWidget {
+  const _OptionListItem(
+    this.value, {
+    Key? key,
+  }) : super(key: key);
+
+  final T value;
+
+  @override
+  Widget build(BuildContext context) {
+    if (T == String || T == OptionItem) {
+      var children = <Widget>[];
+      if (value is String) {
+        children = [
+          Text(value as String),
+        ];
+      } else if (value is OptionItem) {
+        final optionItem = value as OptionItem;
+        children = [
+          if (optionItem.icon != null) optionItem.icon!,
+          Text(optionItem.title),
+        ];
+      }
+      return Column(
+        mainAxisSize: MainAxisSize.min,
+        children: children,
+      );
+    }
+    throw UnimplementedError('The type $T is not supported by option list.');
+  }
+}