feat(editor): FaiEditorStyle + frosted glass panel + pattern/zoom dropdowns
Visual-effect knobs (FaiEditorStyle) the host can override — distinct from ColorScheme, which stays orthogonal. Today Studio passes it through FlowEditorPage's new style: parameter; tomorrow a theme plugin can ship both colours and effects in one move. Two presets ship today: modern (frosted glass, gradient backdrop, animated flow, shadowed nodes — the default) and minimal (everything off, flat surfaces — accessibility / reduce-motion / low-spec). Properties panel rebuilt as a floating sidebar with BackdropFilter blur when glass-style is on; falls back to the pre-0.7 flush-divider layout under solid style. Pattern + zoom controls promoted to PopupMenuButton dropdowns so operators reach a specific zoom level / pattern in one click instead of cycling. Bumps fai_studio_flow_editor to 0.8.0. Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
parent
7aeeae717f
commit
1f5601a461
6 changed files with 386 additions and 130 deletions
|
|
@ -23,12 +23,14 @@
|
|||
library;
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_code_editor/flutter_code_editor.dart';
|
||||
|
||||
import 'editor_controller.dart';
|
||||
import 'editor_style.dart';
|
||||
import 'l10n.dart';
|
||||
import 'model/flow_graph.dart';
|
||||
import 'run_driver.dart';
|
||||
|
|
@ -65,12 +67,21 @@ class FlowEditorPage extends StatefulWidget {
|
|||
/// Empty list = the picker shows a free-form text field.
|
||||
final List<String> availableCapabilities;
|
||||
|
||||
/// Visual-effect overrides — frosted glass on / off, canvas
|
||||
/// backdrop style, flow animation on / off. When null the
|
||||
/// package's [FaiEditorStyle.modern] preset is used. A
|
||||
/// theme-plugin host can pass its own to flip the editor
|
||||
/// between Studio's active style modes without touching
|
||||
/// the editor's source.
|
||||
final FaiEditorStyle? style;
|
||||
|
||||
const FlowEditorPage({
|
||||
super.key,
|
||||
this.initialFlowName,
|
||||
this.locale = FlowEditorLocale.en,
|
||||
this.runDriver,
|
||||
this.availableCapabilities = const [],
|
||||
this.style,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -432,71 +443,119 @@ outputs:
|
|||
}
|
||||
|
||||
Widget _graphTab(ThemeData theme) {
|
||||
// When a step is selected, show the properties panel on
|
||||
// the right as a fixed sidebar. Selection is owned by
|
||||
// the controller and ChangeNotifier rebuilds drive the
|
||||
// panel show / hide. When the flow has no steps at all,
|
||||
// overlay a call-to-action so the operator's first
|
||||
// instinct is the right action rather than staring at an
|
||||
// empty grid.
|
||||
// When a step is selected, show the properties panel as a
|
||||
// floating sidebar on the right. In glass-style mode the
|
||||
// panel overlaps the canvas so the BackdropFilter has
|
||||
// canvas content to blur. In solid-style mode the panel
|
||||
// sits flush against the canvas with a divider — no
|
||||
// overlap needed.
|
||||
//
|
||||
// When the flow has no steps at all, overlay a CTA so the
|
||||
// operator's first instinct is the right action rather
|
||||
// than staring at an empty grid.
|
||||
final hasSteps = _controller.graph.steps.isNotEmpty;
|
||||
final style = widget.style ?? FaiEditorStyle.modern;
|
||||
final glass = style.panelStyle == EditorPanelStyle.glass;
|
||||
final hasSelection = _controller.selectedStepId != null;
|
||||
|
||||
Widget emptyOverlay() => Positioned.fill(
|
||||
child: Container(
|
||||
color: theme.colorScheme.surface.withValues(alpha: 0.92),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.account_tree_outlined,
|
||||
size: 48,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(height: FaiSpace.md),
|
||||
Text(_l.graphEmptyTitle, style: theme.textTheme.titleMedium),
|
||||
const SizedBox(height: FaiSpace.sm),
|
||||
Text(
|
||||
_l.graphEmptyBody,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: FaiSpace.md),
|
||||
FilledButton.icon(
|
||||
onPressed: _addStep,
|
||||
icon: const Icon(Icons.add_box_outlined, size: 16),
|
||||
label: Text(_l.addStep),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget panel() => PropertiesPanel(
|
||||
controller: _controller,
|
||||
strings: _l,
|
||||
availableCapabilities: widget.availableCapabilities,
|
||||
);
|
||||
|
||||
if (glass) {
|
||||
// Stack layout so the frosted panel can overlap canvas.
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: FlowCanvas(controller: _controller, style: style),
|
||||
),
|
||||
if (!hasSteps) emptyOverlay(),
|
||||
if (hasSelection)
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: 320,
|
||||
child: ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: style.panelBlurSigma,
|
||||
sigmaY: style.panelBlurSigma,
|
||||
),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface.withValues(
|
||||
alpha: style.panelBackgroundAlpha,
|
||||
),
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: theme.colorScheme.outlineVariant.withValues(
|
||||
alpha: 0.5,
|
||||
),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: panel(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Solid-style: flush sidebar, divider, no blur.
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
FlowCanvas(controller: _controller),
|
||||
if (!hasSteps)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: theme.colorScheme.surface.withValues(alpha: 0.92),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.account_tree_outlined,
|
||||
size: 48,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(height: FaiSpace.md),
|
||||
Text(
|
||||
_l.graphEmptyTitle,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: FaiSpace.sm),
|
||||
Text(
|
||||
_l.graphEmptyBody,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: FaiSpace.md),
|
||||
FilledButton.icon(
|
||||
onPressed: _addStep,
|
||||
icon: const Icon(Icons.add_box_outlined, size: 16),
|
||||
label: Text(_l.addStep),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
FlowCanvas(controller: _controller, style: style),
|
||||
if (!hasSteps) emptyOverlay(),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_controller.selectedStepId != null) ...[
|
||||
if (hasSelection) ...[
|
||||
const VerticalDivider(width: 1),
|
||||
SizedBox(
|
||||
width: 320,
|
||||
child: PropertiesPanel(
|
||||
controller: _controller,
|
||||
strings: _l,
|
||||
availableCapabilities: widget.availableCapabilities,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 320, child: panel()),
|
||||
],
|
||||
],
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue