Hierarchical Input Widget #71

Open
opened 2026-05-14 16:45:12 +00:00 by guettli · 2 comments
guettli commented 2026-05-14 16:45:12 +00:00 (Migrated from codeberg.org)

When looking at a mail, or pressing on a mail in the list-view, there should open a hierarchical option list around the place where the user pushed the screen.

This input should be a generic input widget, and should be in its own git repo.

The possiblities of inputs should be a DAG.

Each level should not have more than 8 items, but it is up to the widget user to decide that.

Roughly 1.5cm around the finger of the user there should be N items. Moving to one item, selects it. Now there could be again N sub-items or the item is a final item. Then it the selected one.

How to develop something like that, so that it is re-usable in almost all Flutter applications?

Create a plan. Don't implement.

Are there already similar input widgets?

Set Label "State/Question" when done.

When looking at a mail, or pressing on a mail in the list-view, there should open a hierarchical option list around the place where the user pushed the screen. This input should be a generic input widget, and should be in its own git repo. The possiblities of inputs should be a DAG. Each level should not have more than 8 items, but it is up to the widget user to decide that. Roughly 1.5cm around the finger of the user there should be N items. Moving to one item, selects it. Now there could be again N sub-items or the item is a final item. Then it the selected one. How to develop something like that, so that it is re-usable in almost all Flutter applications? Create a plan. Don't implement. Are there already similar input widgets? Set Label "State/Question" when done.
guettlibot commented 2026-05-14 17:52:06 +00:00 (Migrated from codeberg.org)

Existing similar widgets

Package Hierarchy Status
pie_menu v3.7.0 Active (15 days ago)
star_menu ✓ sub-menus ~2 years ago
radial_menu / flutter_radial_menu 5–8 years old, Dart 3 incompatible

star_menu supports nested/hierarchical menus but is aging. pie_menu is actively maintained but flat only. Neither supports a DAG structure or the continuous-drag-to-select gesture described.


Plan: dag_pie Flutter package

Concept: A radial overlay widget where items appear ~1.5 cm around the touch point. Dragging the finger toward an item hovers/selects it. Releasing on a leaf node returns the selection; releasing on an intermediate node replaces the current ring with its children (breadcrumb navigation through the DAG).

Public API sketch

DagPie(
  dag: DagPieGraph(nodes: [
    DagPieNode(id: reply, label: "Reply", icon: Icons.reply),
    DagPieNode(id: reply-all, label: "Reply All", icon: Icons.reply_all, parentIds: ["reply"]),
    DagPieNode(id: move, label: "Move", icon: Icons.folder),
    DagPieNode(id: move-inbox, label: "Inbox", parentIds: ["move"]),
    DagPieNode(id: move-trash, label: "Trash", icon: Icons.delete, parentIds: ["move"]),
  ]),
  onSelected: (nodeId) {  },
  child: EmailListTile(),
)

Key implementation pieces

  1. Gesture layerGestureDetector on LongPress or onTapDown to record touch point; GestureDetector.onPanUpdate to track drag direction and highlight the nearest item.
  2. OverlayOverlayEntry positioned at the touch point, drawn with CustomPainter (arcs + icons). No showDialog — must not block the rest of the UI.
  3. Layout — N items distributed evenly on a circle of radius ≈ 1.5 cm (MediaQuery.devicePixelRatio + PlatformDispatcher.views for physical-to-logical conversion). Clamp to screen edges.
  4. DAG navigation — Keep a currentParentId stack. On hover of an intermediate node, animate the current ring out and the children ring in. Back-swipe or centre-tap goes up a level.
  5. AnimationAnimationController + CurvedAnimation for ring appear/disappear; TweenAnimationBuilder for item highlight.
  6. Separate package — Publish as dag_pie (or similar) with zero dependencies on SharedInbox internals. Only flutter SDK.

Open questions before implementation

  • Should the gesture be long-press then drag (like a context menu) or tap-and-hold?
  • Should dragging off all items cancel, or keep the last hovered item?
  • Max 8 items is a guideline — should the widget enforce it (assert) or warn?
  • DAG vs tree: if the same node appears under multiple parents, should it render twice or show a "shared" indicator?

Setting State/Question — please review the plan and answer the open questions before I start implementation.

## Existing similar widgets | Package | Hierarchy | Status | |---------|-----------|--------| | [pie_menu](https://pub.dev/packages/pie_menu) v3.7.0 | ✗ | Active (15 days ago) | | [star_menu](https://pub.dev/packages/star_menu) | ✓ sub-menus | ~2 years ago | | radial_menu / flutter_radial_menu | ✗ | 5–8 years old, Dart 3 incompatible | `star_menu` supports nested/hierarchical menus but is aging. `pie_menu` is actively maintained but flat only. Neither supports a DAG structure or the continuous-drag-to-select gesture described. --- ## Plan: `dag_pie` Flutter package **Concept:** A radial overlay widget where items appear ~1.5 cm around the touch point. Dragging the finger toward an item hovers/selects it. Releasing on a leaf node returns the selection; releasing on an intermediate node replaces the current ring with its children (breadcrumb navigation through the DAG). ### Public API sketch ```dart DagPie( dag: DagPieGraph(nodes: [ DagPieNode(id: reply, label: "Reply", icon: Icons.reply), DagPieNode(id: reply-all, label: "Reply All", icon: Icons.reply_all, parentIds: ["reply"]), DagPieNode(id: move, label: "Move", icon: Icons.folder), DagPieNode(id: move-inbox, label: "Inbox", parentIds: ["move"]), DagPieNode(id: move-trash, label: "Trash", icon: Icons.delete, parentIds: ["move"]), ]), onSelected: (nodeId) { … }, child: EmailListTile(…), ) ``` ### Key implementation pieces 1. **Gesture layer** — `GestureDetector` on `LongPress` or `onTapDown` to record touch point; `GestureDetector.onPanUpdate` to track drag direction and highlight the nearest item. 2. **Overlay** — `OverlayEntry` positioned at the touch point, drawn with `CustomPainter` (arcs + icons). No `showDialog` — must not block the rest of the UI. 3. **Layout** — N items distributed evenly on a circle of radius ≈ 1.5 cm (`MediaQuery.devicePixelRatio` + `PlatformDispatcher.views` for physical-to-logical conversion). Clamp to screen edges. 4. **DAG navigation** — Keep a `currentParentId` stack. On hover of an intermediate node, animate the current ring out and the children ring in. Back-swipe or centre-tap goes up a level. 5. **Animation** — `AnimationController` + `CurvedAnimation` for ring appear/disappear; `TweenAnimationBuilder` for item highlight. 6. **Separate package** — Publish as `dag_pie` (or similar) with zero dependencies on SharedInbox internals. Only `flutter` SDK. ### Open questions before implementation - Should the gesture be **long-press then drag** (like a context menu) or **tap-and-hold**? - Should dragging off all items cancel, or keep the last hovered item? - Max 8 items is a guideline — should the widget enforce it (assert) or warn? - DAG vs tree: if the same node appears under multiple parents, should it render twice or show a "shared" indicator? Setting State/Question — please review the plan and answer the open questions before I start implementation.
guettlibot commented 2026-05-21 05:15:51 +00:00 (Migrated from codeberg.org)

Plan: Hierarchical Radial Input Widget (standalone Flutter package)

Existing similar widgets

Package / project What it does Gap
pie_menu (pub.dev) Circular radial menu around a tap point, flat list of actions No hierarchy / DAG, no sub-levels
radial_menu (pub.dev) Animated radial menu, single level No hierarchy
flutter_context_menu Dropdown list context menu Not radial, not finger-based
macOS/iOS UIContextMenu Hierarchical popover on long-press Only Apple platforms, not Flutter

None of the existing packages support a multi-level DAG with sub-menus opening around the finger position. A new package is warranted.


Design

Interaction model:

  1. User taps or long-presses anywhere (on a mail, on the list, etc.).
  2. A ring of N ≤ 8 icons/labels appears centred ~1.5 cm from the touch point.
  3. User slides (without lifting finger) towards an item — it highlights on hover.
  4. If the item is a leaf → it is selected; overlay closes; callback fires.
  5. If the item has children → the ring re-centres on that item and shows its children.
  6. User can slide back to the parent by moving back towards the centre.
  7. Lifting the finger outside all items cancels; pressing Escape or back also cancels.

DAG model:

class RadialItem {
  final Widget icon;        // shown inside the ring segment
  final String label;       // shown as tooltip / text below icon
  final List<RadialItem> children;  // empty = leaf
  final VoidCallback? onSelected;   // only meaningful for leaves
}

Callers pass a root List<RadialItem> (up to 8 items per level). Each item can have its own children. The widget enforces the ≤ 8 limit; extras are silently dropped or trigger an assertion in debug mode.


Layout algorithm

  • Ring radius: 1.5 cm = physicalSize / devicePixelRatio * 56 (approx 56 logical px ≈ 1.5 cm on a 150 dpi screen; configurable).
  • Item positions: evenly spaced around the ring, rotated so the item nearest the screen centre is at top (avoids finger occlusion).
  • Hit testing: each item is a 48×48 dp touch target (Material minimum); overlap handled by proximity to arc midpoint.
  • Opening animation: items fly out from the tap point over ~150 ms (curved easing).

API (package public surface)

// Show the overlay. Returns the selected leaf item or null if cancelled.
Future<RadialItem?> showRadialMenu({
  required BuildContext context,
  required Offset position,   // tap position in global coordinates
  required List<RadialItem> items,
  double radiusPx = 56,       // ~1.5 cm
  int maxItemsPerLevel = 8,
});

// Convenience wrapper — use inside GestureDetector.onLongPressStart.
class RadialMenuGestureDetector extends StatelessWidget {
  final List<RadialItem> items;
  final Widget child;
  // ...
}

Repository structure (standalone package)

flutter_radial_dag_menu/
  lib/
    flutter_radial_dag_menu.dart   ← public API
    src/
      radial_overlay.dart
      ring_layout.dart
      item_widget.dart
  example/
    lib/main.dart                  ← demo with a 3-level DAG
  test/
    ring_layout_test.dart
  pubspec.yaml
  README.md

Hosted on Codeberg (or pub.dev if published). In sharedinbox, add it as a path dependency initially, then switch to a version dependency once stable.


Open questions (setting label "State/Question")

  1. Slide-to-select vs tap-to-select? Slide (no finger lift) is great for speed but hard to discover. Tap-to-select (lift, then tap item) is more standard. Which does the user prefer?
  2. Back navigation: slide back to centre, or a dedicated "back" item in the ring?
  3. Accessibility: radial menus are difficult for users with motor impairments. Should there be a fallback linear list mode?
  4. Platform targets: Android + Linux (current platforms in sharedinbox). Mouse support needed for Linux?
## Plan: Hierarchical Radial Input Widget (standalone Flutter package) ### Existing similar widgets | Package / project | What it does | Gap | |-------------------|--------------|-----| | `pie_menu` (pub.dev) | Circular radial menu around a tap point, flat list of actions | No hierarchy / DAG, no sub-levels | | `radial_menu` (pub.dev) | Animated radial menu, single level | No hierarchy | | `flutter_context_menu` | Dropdown list context menu | Not radial, not finger-based | | macOS/iOS UIContextMenu | Hierarchical popover on long-press | Only Apple platforms, not Flutter | None of the existing packages support a multi-level DAG with sub-menus opening around the finger position. A new package is warranted. --- ### Design **Interaction model:** 1. User taps or long-presses anywhere (on a mail, on the list, etc.). 2. A ring of **N ≤ 8 icons/labels** appears centred ~1.5 cm from the touch point. 3. User slides (without lifting finger) towards an item — it highlights on hover. 4. **If the item is a leaf** → it is selected; overlay closes; callback fires. 5. **If the item has children** → the ring re-centres on that item and shows its children. 6. User can slide back to the parent by moving back towards the centre. 7. Lifting the finger outside all items cancels; pressing Escape or back also cancels. **DAG model:** ```dart class RadialItem { final Widget icon; // shown inside the ring segment final String label; // shown as tooltip / text below icon final List<RadialItem> children; // empty = leaf final VoidCallback? onSelected; // only meaningful for leaves } ``` Callers pass a root `List<RadialItem>` (up to 8 items per level). Each item can have its own children. The widget enforces the ≤ 8 limit; extras are silently dropped or trigger an assertion in debug mode. --- ### Layout algorithm - **Ring radius:** `1.5 cm` = `physicalSize / devicePixelRatio * 56` (approx 56 logical px ≈ 1.5 cm on a 150 dpi screen; configurable). - **Item positions:** evenly spaced around the ring, rotated so the item nearest the screen centre is at top (avoids finger occlusion). - **Hit testing:** each item is a 48×48 dp touch target (Material minimum); overlap handled by proximity to arc midpoint. - **Opening animation:** items fly out from the tap point over ~150 ms (curved easing). --- ### API (package public surface) ```dart // Show the overlay. Returns the selected leaf item or null if cancelled. Future<RadialItem?> showRadialMenu({ required BuildContext context, required Offset position, // tap position in global coordinates required List<RadialItem> items, double radiusPx = 56, // ~1.5 cm int maxItemsPerLevel = 8, }); // Convenience wrapper — use inside GestureDetector.onLongPressStart. class RadialMenuGestureDetector extends StatelessWidget { final List<RadialItem> items; final Widget child; // ... } ``` --- ### Repository structure (standalone package) ``` flutter_radial_dag_menu/ lib/ flutter_radial_dag_menu.dart ← public API src/ radial_overlay.dart ring_layout.dart item_widget.dart example/ lib/main.dart ← demo with a 3-level DAG test/ ring_layout_test.dart pubspec.yaml README.md ``` Hosted on Codeberg (or pub.dev if published). In `sharedinbox`, add it as a path dependency initially, then switch to a version dependency once stable. --- ### Open questions (setting label "State/Question") 1. **Slide-to-select vs tap-to-select?** Slide (no finger lift) is great for speed but hard to discover. Tap-to-select (lift, then tap item) is more standard. Which does the user prefer? 2. **Back navigation:** slide back to centre, or a dedicated "back" item in the ring? 3. **Accessibility:** radial menus are difficult for users with motor impairments. Should there be a fallback linear list mode? 4. **Platform targets:** Android + Linux (current platforms in sharedinbox). Mouse support needed for Linux?
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: guettli/sharedinbox#71