native_adaptive_toolbox 0.1.0
native_adaptive_toolbox: ^0.1.0 copied to clipboard
Native text-selection menus and custom-painted handles, foundation-only.
native_adaptive_toolbox #
Native text-selection menus and custom-painted selection handles for Flutter, using only the foundation, services, widgets, and rendering layers. No Material, no Cupertino, no custom-drawn menu, no fallback.
What it does #
| Platform | Menu | Handles | Manually tested |
|---|---|---|---|
| Android | Native floating ActionMode toolbar | Custom-painted grip | ✅ |
| iOS | UIEditMenuInteraction / UIMenuController |
Custom-painted grip | ❌ |
| macOS | NSMenu |
none (desktop) | ❌ |
| Windows | TrackPopupMenu |
none (desktop) | ❌ |
| Linux | GTK menu | none (desktop) | ✅ |
| Web | Browser-owned native menu | — | ✅ |
The menu is the platform's own. Flutter computes the anchor — the selection
bounds, or the exact right-click point for context menus — keeps it inside the
visible area (never under the IME), and hands it to the platform over the
native_adaptive_toolbox/menu channel. Context menus keep the click point at
the menu's corner: top-left normally, top-right for RTL or when the menu would
overflow the window's edge (measured with the menu's real size, like web).
There is no custom-drawn menu anywhere; a platform without an implementation
returns false from showMenu and no menu appears.
Usage #
Native context menu at a point #
import 'package:native_adaptive_toolbox/native_adaptive_toolbox.dart';
final (anchor, topRight) = NativeToolboxPositioner.resolvePointAnchor(
clickPosition, // global logical coordinates
visibleBounds, // screen minus IME insets minus padding
rtl: false,
);
final shown = await NativeToolboxMenu.showMenu(
anchorRect: Rect.fromPoints(anchor, anchor),
actions: const [
NativeToolboxAction(id: 'copy', label: 'Copy'),
NativeToolboxAction(id: 'cut', label: 'Cut'),
NativeToolboxAction(id: 'paste', label: 'Paste'),
NativeToolboxAction(id: 'selectAll', label: 'Select all'),
],
selectionMode: true,
topRight: topRight,
);
if (!shown) {
// No platform implementation on this device.
}
NativeToolboxMenu.onAction.listen((index) {
// index into the actions list; the platform already dismissed the menu.
});
NativeToolboxMenu.onDismiss.listen((_) {
// The user dismissed the menu without choosing an action.
});
Selection menu anchored to a selection #
final anchor = NativeToolboxPositioner.resolveAnchor(
selectionBounds, // the selection rect in global logical coordinates
visibleBounds,
);
await NativeToolboxMenu.showMenu(
anchorRect: anchor,
actions: actions,
selectionMode: true,
);
Foundation-only selection handles #
NativeToolboxSelectionControls implements the widgets-layer
TextSelectionControls contract with custom-painted grips (Android 22×22, iOS
24×24, desktop none), so any custom text surface can plug them in:
final controls = NativeToolboxSelectionControls(
platform: defaultTargetPlatform,
handleBuilder: (context, type, lineHeight) => myCustomHandle(context),
);
A host handle builder replaces only the visual; geometry, hit targets, and dragging stay with the caller's selection overlay.
Positioning #
resolveAnchor(selectionBounds, visibleBounds)— clamps the selection rect into the visible bounds.resolvePointAnchor(point, visibleBounds, rtl:)— clamps a context-menu point; returns whether the menu should extend leftward (RTL). The near-the-edge flip is decided per platform with the menu's real size.
The anchor is in FlutterView-global logical coordinates; the channel payload carries physical pixels plus the view offset so each platform converts into its own window coordinate space.
Development #
dart format --output=none --set-exit-if-changed .
flutter analyze
flutter test
cd example && flutter test
cd example && flutter build web --release
dart pub publish --dry-run
Device acceptance for each platform: long-press or right-click a selection, verify the native menu appears at the anchor, actions execute once, dismissal is reported, and the menu stays above the IME.
See CONTRIBUTING.md and the MIT LICENSE.