diff --git a/lib/src/flow_editor_page.dart b/lib/src/flow_editor_page.dart index d1e1344..354d4e8 100644 --- a/lib/src/flow_editor_page.dart +++ b/lib/src/flow_editor_page.dart @@ -357,17 +357,10 @@ outputs: strings: _l, activeName: _controller.activeName, dirty: _controller.isDirty, - saving: _controller.isSaving, - running: _controller.isRunning, onBack: Navigator.of(context).canPop() ? () => Navigator.of(context).maybePop() : null, - onSave: _controller.activeName != null ? _save : null, - onAddStep: _controller.activeName != null ? _addStep : null, onNew: _newFlow, - onRun: _controller.activeName != null - ? () => _tabs.animateTo(2) - : null, ), const Divider(height: 1), Expanded( @@ -421,6 +414,17 @@ outputs: ], ), ), + _TabActionStrip( + strings: _l, + tabs: _tabs, + saving: _controller.isSaving, + running: _controller.isRunning, + onAddStep: _controller.activeName != null ? _addStep : null, + onSave: _controller.activeName != null ? _save : null, + onRun: _controller.activeName != null + ? () => _tabs.animateTo(2) + : null, + ), Expanded( child: TabBarView( controller: _tabs, @@ -688,24 +692,14 @@ class _Toolbar extends StatelessWidget { final FlowEditorStrings strings; final String? activeName; final bool dirty; - final bool saving; - final bool running; final VoidCallback? onBack; - final VoidCallback? onSave; - final VoidCallback? onAddStep; final VoidCallback onNew; - final VoidCallback? onRun; const _Toolbar({ required this.strings, required this.activeName, required this.dirty, - required this.saving, - required this.running, required this.onBack, - required this.onSave, - required this.onAddStep, required this.onNew, - required this.onRun, }); @override @@ -776,43 +770,106 @@ class _Toolbar extends StatelessWidget { icon: const Icon(Icons.add, size: 16), label: Text(strings.newFlow), ), - const SizedBox(width: FaiSpace.sm), - if (onAddStep != null) - FilledButton.tonalIcon( - onPressed: onAddStep, - icon: const Icon(Icons.add_box_outlined, size: 16), - label: Text(strings.addStep), - ), - const SizedBox(width: FaiSpace.sm), - FilledButton.tonalIcon( - onPressed: saving ? null : onSave, - icon: saving - ? const SizedBox( - width: 14, - height: 14, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.save_outlined, size: 16), - label: Text(strings.save), - ), - const SizedBox(width: FaiSpace.sm), - FilledButton.icon( - onPressed: onRun, - icon: running - ? const SizedBox( - width: 14, - height: 14, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.play_arrow, size: 18), - label: Text(strings.run), - ), ], ), ); } } +// Tab-aware action strip — lives directly under the TabBar so +// the operator's eye flows from TabBar → context buttons → +// canvas. Each tab exposes a different set of actions: +// +// Graph: [+ Step] [Save] [Run] +// Text: [Save] [Run] +// Run: (no buttons — Run tab has its own start button) +// +// Rebuilds when the TabController index changes via the +// passed-in animation. +class _TabActionStrip extends StatelessWidget { + final FlowEditorStrings strings; + final TabController tabs; + final bool saving; + final bool running; + final VoidCallback? onAddStep; + final VoidCallback? onSave; + final VoidCallback? onRun; + const _TabActionStrip({ + required this.strings, + required this.tabs, + required this.saving, + required this.running, + required this.onAddStep, + required this.onSave, + required this.onRun, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return AnimatedBuilder( + animation: tabs.animation ?? tabs, + builder: (context, _) { + final idx = tabs.index; + final showAddStep = idx == 0; + final showSaveRun = idx == 0 || idx == 1; + return Container( + padding: const EdgeInsets.symmetric( + horizontal: FaiSpace.md, + vertical: FaiSpace.sm, + ), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerLow, + border: Border( + bottom: BorderSide( + color: theme.colorScheme.outlineVariant.withValues(alpha: 0.4), + ), + ), + ), + child: Row( + children: [ + if (showAddStep) ...[ + FilledButton.tonalIcon( + onPressed: onAddStep, + icon: const Icon(Icons.add_box_outlined, size: 16), + label: Text(strings.addStep), + ), + const SizedBox(width: FaiSpace.sm), + ], + if (showSaveRun) ...[ + FilledButton.tonalIcon( + onPressed: saving ? null : onSave, + icon: saving + ? const SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.save_outlined, size: 16), + label: Text(strings.save), + ), + const SizedBox(width: FaiSpace.sm), + FilledButton.icon( + onPressed: onRun, + icon: running + ? const SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.play_arrow, size: 18), + label: Text(strings.run), + ), + ], + const Spacer(), + ], + ), + ); + }, + ); + } +} + // --- file list --- class _FlowFile { diff --git a/lib/src/widgets/flow_canvas.dart b/lib/src/widgets/flow_canvas.dart index c610cdd..454c86e 100644 --- a/lib/src/widgets/flow_canvas.dart +++ b/lib/src/widgets/flow_canvas.dart @@ -429,26 +429,6 @@ class _FlowCanvasState extends State : BoxDecoration(color: theme.colorScheme.surface), child: Stack( children: [ - // Outer-most tap layer — catches clicks on the - // dark area OUTSIDE the canvas grid (when the - // operator has panned / zoomed so the grid - // doesn't fill the viewport). Translucent so - // InteractiveViewer below still handles pan + - // zoom; the gesture arena gives the tap to - // whichever child claims it first, and on - // canvas-empty pointers nobody else does. - Positioned.fill( - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - // Deselect both step + edge so the - // properties panel closes when the operator - // clicks the empty backdrop. - widget.controller.selectStep(null); - widget.controller.selectEdge(null); - }, - ), - ), InteractiveViewer( transformationController: _transform, constrained: false, diff --git a/pubspec.yaml b/pubspec.yaml index 5c2792a..b9ef833 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: fai_studio_flow_editor description: Swappable inline YAML editor for F∆I Studio flows. -version: 0.13.0 +version: 0.14.0 publish_to: 'none' repository: https://git.flemming.ai/fai/studio-flow-editor