Skip to content

Commit 1c9628a

Browse files
Panteclaude
andauthored
Add FOverlay widget (#935)
* Move portal * Add FOverlay and document overlay positioning caveat Extract CompositedOverlay from CompositedPortal as a simpler base that composites at the child's origin without positioning logic. Add FOverlay widget that works like a Stack layered on top of the child in the overlay, with hit testing support for overflowing children. Document that Positioned.bottom/right can flicker when the child resizes because the overlay's layout may read a stale child size (scheduleTask deferral in _schedule is required since markNeedsLayout cannot be called during paint/layout of a sibling render object). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Prepare Docs Snippets for review * Prepare Forui for review * Fix PR issues --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Pante <Pante@users.noreply.github.com>
1 parent ff0d87f commit 1c9628a

21 files changed

Lines changed: 731 additions & 240 deletions
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
title: Overlay
3+
description: A low-level overlay primitive that composites content relative to a child widget.
4+
apiReference: https://pub.dev/documentation/forui/latest/forui.foundation/FOverlay-class.html
5+
---
6+
7+
import { Tabs, Tab } from 'fumadocs-ui/components/tabs';
8+
import { Callout } from 'fumadocs-ui/components/callout';
9+
import { Widget } from '@/components/demo/widget';
10+
import { CodeSnippet } from '@/components/code-snippet/code-snippet';
11+
import { UsageSnippet } from '@/components/usage-snippet/usage-snippet';
12+
import overlayDefault from '@/snippets/examples/overlay/default.json';
13+
import overlayUsage from '@/snippets/usages/foundation/overlay/overlay.json';
14+
15+
<Callout type="info">
16+
This widget is typically used to create other high-level widgets. For higher-level positioning with anchor alignment
17+
and overflow handling, use [FPortal](../foundation/portal).
18+
</Callout>
19+
20+
<Tabs items={['Preview', 'Code']}>
21+
<Tab value="Preview">
22+
<Widget name='overlay' height={300}/>
23+
</Tab>
24+
<Tab value="Code">
25+
<CodeSnippet snippet={overlayDefault} />
26+
</Tab>
27+
</Tabs>
28+
29+
## Usage
30+
31+
### `FOverlay(...)`
32+
<UsageSnippet usage={overlayUsage} />
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import 'package:flutter/material.dart';
2+
3+
import 'package:auto_route/auto_route.dart';
4+
import 'package:forui/forui.dart';
5+
6+
import 'package:docs_snippets/example.dart';
7+
8+
@RoutePage()
9+
class OverlayPage extends StatefulExample {
10+
OverlayPage({@queryParam super.theme});
11+
12+
@override
13+
State<OverlayPage> createState() => _State();
14+
}
15+
16+
class _State extends StatefulExampleState<OverlayPage> {
17+
@override
18+
Widget example(BuildContext context) => FOverlay(
19+
overlay: [
20+
Positioned(
21+
top: -50,
22+
left: 0,
23+
child: Container(
24+
padding: const .symmetric(horizontal: 12, vertical: 8),
25+
decoration: BoxDecoration(
26+
color: context.theme.colors.background,
27+
border: .all(color: context.theme.colors.border),
28+
borderRadius: .circular(4),
29+
),
30+
child: Text('Overlay content', style: context.theme.typography.sm),
31+
),
32+
),
33+
],
34+
builder: (context, controller, _) => FButton(
35+
variant: .outline,
36+
size: .sm,
37+
mainAxisSize: .min,
38+
onPress: controller.toggle,
39+
child: const Text('Toggle Overlay'),
40+
),
41+
);
42+
}

docs_snippets/lib/main.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import 'package:flutter/material.dart' hide DialogRoute;
1+
import 'package:flutter/material.dart' hide DialogRoute, OverlayRoute;
22

33
import 'package:auto_route/auto_route.dart';
44
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
@@ -160,6 +160,7 @@ class _AppRouter extends RootStackRouter {
160160
AutoRoute(path: '/popover-menu/blurred', page: BlurredPopoverMenuRoute.page),
161161
AutoRoute(path: '/popover-menu/nested', page: NestedPopoverMenuRoute.page),
162162
AutoRoute(path: '/popover-menu/tiles', page: TilePopoverMenuRoute.page),
163+
AutoRoute(path: '/overlay/default', page: OverlayRoute.page),
163164
AutoRoute(path: '/portal/default', page: PortalRoute.page),
164165
AutoRoute(path: '/portal-visualization/default', page: PortalVisualizationRoute.page),
165166
AutoRoute(path: '/progress/default', page: ProgressRoute.page),
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// ignore_for_file: avoid_redundant_argument_values
2+
3+
import 'package:flutter/widgets.dart';
4+
5+
import 'package:forui/forui.dart';
6+
7+
final overlay = FOverlay(
8+
// {@category "Core"}
9+
controller: null,
10+
overlay: const [Positioned(top: 42, left: 0, child: Text('Overlay content'))],
11+
overlayBuilder: (context, controller, childRenderBox, overlay) => overlay,
12+
builder: (context, controller, child) => child!,
13+
child: const Text('Child'),
14+
// {@endcategory}
15+
);

forui/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@
2424
* Add `FTileMixin.submenu(...)` shorthand for `FSubmenuTile`.
2525

2626

27+
### `FOverlay`
28+
* Add `FOverlay`.
29+
30+
31+
### `FPortal`
32+
* Fix portal not repositioning when the child widget changes size.
33+
34+
2735
## 0.20.3
2836

2937
### `FToaster`

forui/lib/foundation.dart

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ export 'src/foundation/tappable/tappable_group.dart' hide GroupEntry, TappableGr
1313
export 'src/foundation/time.dart';
1414
export 'src/foundation/tween.dart';
1515
export 'src/foundation/typeahead_controller.dart';
16-
export 'src/foundation/portal/portal.dart';
17-
export 'src/foundation/portal/portal_constraints.dart' hide FixedConstraints;
18-
export 'src/foundation/portal/portal_overflow.dart';
19-
export 'src/foundation/portal/portal_spacing.dart';
16+
export 'src/foundation/overlay/overlay.dart';
17+
export 'src/foundation/overlay/portal.dart';
18+
export 'src/foundation/overlay/portal_constraints.dart' hide FixedConstraints;
19+
export 'src/foundation/overlay/portal_overflow.dart';
20+
export 'src/foundation/overlay/portal_spacing.dart';

forui/lib/src/foundation/portal/composited_child.dart renamed to forui/lib/src/foundation/overlay/composited_child.dart

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import 'package:flutter/widgets.dart';
66
import 'package:meta/meta.dart';
77

88
import 'package:forui/forui.dart';
9-
import 'package:forui/src/foundation/portal/composited_portal.dart';
10-
import 'package:forui/src/foundation/portal/layer.dart';
9+
import 'package:forui/src/foundation/overlay/composited_portal.dart';
10+
import 'package:forui/src/foundation/overlay/layer.dart';
1111

1212
/// A [CompositedChild] allows [CompositedPortal]s to position themselves relative to it.
1313
///
@@ -62,6 +62,7 @@ class RenderChildLayer extends RenderProxyBox {
6262
/// changes.
6363
Size _viewSize;
6464
Offset? _previousGlobalOffset;
65+
Size? _previousPaintSize;
6566

6667
RenderChildLayer({
6768
required FChangeNotifier notifier,
@@ -94,16 +95,18 @@ class RenderChildLayer extends RenderProxyBox {
9495
..localOffset = offset;
9596
}
9697

97-
context.pushLayer(layer!, super.paint, Offset.zero);
98+
context.pushLayer(layer!, super.paint, .zero);
9899
assert(() {
99100
layer!.debugCreator = debugCreator;
100101
return true;
101102
}());
102103

103-
if (globalOffset != _previousGlobalOffset) {
104+
if (globalOffset != _previousGlobalOffset || size != _previousPaintSize) {
104105
_previousGlobalOffset = globalOffset;
105-
// Signals to the linked [CompositedPortal]s that they need to repaint. This is requires as the child & portal
106-
// are painted in separate layers and the portal might not re-paint otherwise, i.e. if the child expands in size.
106+
_previousPaintSize = size;
107+
// Signals to the linked [CompositedPortal]s that they need to relayout/repaint. This is required as the child &
108+
// portal are painted in separate layers and the portal might not update otherwise, i.e. if the child changes
109+
// position or size.
107110
//
108111
// We can create a custom notifier that wraps this, but that seems like overkill.
109112
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member

0 commit comments

Comments
 (0)