feat(editor): properties-panel toggle + LabVIEW-style type colours
Two visible UX fixes:
1. Properties-panel toggle. Tap a step → opens. Tap the
same step again → closes (deselect via toggle). Tap the
canvas background → also closes. selectStep now returns
to null when called with the already-selected id; the
editor-controller test updated for the new semantics.
2. LabVIEW-style type colours on every port + wire.
_typeAccent now picks vibrant brightness-aware hues per
datatype:
text → magenta/pink (LabVIEW string)
json → amber/orange (cluster feel)
bytes → cyan/teal (raw binary)
file → green (file reference)
number → yellow/amber
Step-input dots, step-output dots, and outputs-endpoint
dots all switch from the generic theme.primary to the
field's declared type accent. Operators read the
payload from the dot alone.
Editor tests still pass (20).
Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
parent
7b256b5e35
commit
1e9968496e
6 changed files with 133 additions and 39 deletions
|
|
@ -149,10 +149,14 @@ class FlowEditorController extends ChangeNotifier {
|
|||
notifyListeners();
|
||||
}
|
||||
|
||||
/// User clicked a node on the canvas.
|
||||
/// User clicked a node on the canvas. Clicking the
|
||||
/// already-selected step deselects it (toggle semantics) —
|
||||
/// matches every modern node editor. Pass null explicitly
|
||||
/// to deselect without re-clicking (e.g. background tap).
|
||||
void selectStep(String? stepId) {
|
||||
if (_selectedStepId == stepId) return;
|
||||
_selectedStepId = stepId;
|
||||
final next = stepId != null && _selectedStepId == stepId ? null : stepId;
|
||||
if (_selectedStepId == next) return;
|
||||
_selectedStepId = next;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -617,11 +617,19 @@ outputs:
|
|||
// overrides. Falls back to a balanced light/dark palette
|
||||
// when the scheme tints don't read well as code colours.
|
||||
final keyAccent = cs.primary;
|
||||
final stringAccent = isDark ? const Color(0xFFA5D6A7) : const Color(0xFF2E7D32);
|
||||
final numberAccent = isDark ? const Color(0xFFFFCC80) : const Color(0xFFE65100);
|
||||
final symbolAccent = isDark ? const Color(0xFFB39DDB) : const Color(0xFF6A1B9A);
|
||||
final stringAccent = isDark
|
||||
? const Color(0xFFA5D6A7)
|
||||
: const Color(0xFF2E7D32);
|
||||
final numberAccent = isDark
|
||||
? const Color(0xFFFFCC80)
|
||||
: const Color(0xFFE65100);
|
||||
final symbolAccent = isDark
|
||||
? const Color(0xFFB39DDB)
|
||||
: const Color(0xFF6A1B9A);
|
||||
final commentAccent = cs.onSurfaceVariant.withValues(alpha: 0.7);
|
||||
final metaAccent = isDark ? const Color(0xFF80DEEA) : const Color(0xFF00838F);
|
||||
final metaAccent = isDark
|
||||
? const Color(0xFF80DEEA)
|
||||
: const Color(0xFF00838F);
|
||||
return {
|
||||
'root': monoBase.copyWith(color: cs.onSurface),
|
||||
// YAML keys — bold + primary accent so the structure
|
||||
|
|
|
|||
|
|
@ -471,6 +471,27 @@ class _FlowCanvasState extends State<FlowCanvas>
|
|||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
// Tap on the canvas background (no node,
|
||||
// no port, no edge) deselects the active
|
||||
// step — closes the properties panel.
|
||||
// Tapping a node's hit area is consumed
|
||||
// by the node's own GestureDetector
|
||||
// (opaque), so this fires only on misses.
|
||||
// Edges shadow the same hit-test footprint
|
||||
// via _hitTestEdge; if the cursor was over
|
||||
// an edge we let the edge keep its hover /
|
||||
// context-menu treatment instead of
|
||||
// deselecting.
|
||||
onTapUp: (details) {
|
||||
final edge = _hitTestEdge(
|
||||
details.localPosition,
|
||||
graph,
|
||||
layout,
|
||||
);
|
||||
if (edge == null) {
|
||||
widget.controller.selectStep(null);
|
||||
}
|
||||
},
|
||||
onSecondaryTapDown: (details) {
|
||||
final edge = _hitTestEdge(
|
||||
details.localPosition,
|
||||
|
|
@ -1507,16 +1528,21 @@ class _FlowCanvasState extends State<FlowCanvas>
|
|||
for (final field in labels) {
|
||||
final p = _outputPortPosition(step.id, layout, fieldName: field);
|
||||
final key = '${step.id}:$field';
|
||||
// A specific output is "connected" iff some edge
|
||||
// actually leaves it. The drag handler stamps the
|
||||
// edge's fromField so the new wire is bound to this
|
||||
// specific output rather than the legacy anchor.
|
||||
// Output port colour matches the declared field's
|
||||
// type so operators read the wire's payload from the
|
||||
// dot alone (LabVIEW convention). Falls back to the
|
||||
// theme's primary when the field's type can't be
|
||||
// resolved (legacy v1/v2 manifest, hub not loaded).
|
||||
final type = _stepOutputType(step, field);
|
||||
final accent = type != null
|
||||
? _typeAccent(type, Theme.of(context))
|
||||
: Theme.of(context).colorScheme.primary;
|
||||
yield _portDot(
|
||||
portKey: key,
|
||||
center: p,
|
||||
isSource: true,
|
||||
connected: connectedPorts.contains(key),
|
||||
accent: Theme.of(context).colorScheme.primary,
|
||||
accent: accent,
|
||||
onDragStart: () => _draft = _ConnectionDraft(
|
||||
fromKind: _DraftSourceKind.step,
|
||||
fromId: step.id,
|
||||
|
|
@ -1560,31 +1586,45 @@ class _FlowCanvasState extends State<FlowCanvas>
|
|||
final field = labels[i];
|
||||
final value = step.with_[field]?.toString() ?? '';
|
||||
final wired = _isWiredExpression(value);
|
||||
// Step-input dot wears the declared field's type
|
||||
// colour so a `bytes` slot reads differently from a
|
||||
// `text` slot at a glance.
|
||||
final type = _stepInputType(step, field);
|
||||
final accent = type != null
|
||||
? _typeAccent(type, Theme.of(context))
|
||||
: Theme.of(context).colorScheme.primary;
|
||||
yield _portDot(
|
||||
portKey: '${step.id}:$field',
|
||||
center: p,
|
||||
isSource: false,
|
||||
connected: wired,
|
||||
accent: Theme.of(context).colorScheme.primary,
|
||||
accent: accent,
|
||||
onContextMenu: !wired
|
||||
? null
|
||||
: (pos) => _disconnectInputPort(step.id, field, pos),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Outputs endpoint input ports — same disconnect treatment.
|
||||
// Outputs endpoint input ports — colour by the upstream
|
||||
// type when the YAML's $step.field expression resolves
|
||||
// to a step output whose type we know. Otherwise stays
|
||||
// the theme's primary so the dot is still visible.
|
||||
final outs = graph.outputs.keys.toList();
|
||||
for (var i = 0; i < outs.length; i++) {
|
||||
final p = _inputPortPosition(AutoLayout.outputsNodeId, i, layout);
|
||||
final field = outs[i];
|
||||
final expr = graph.outputs[field] ?? '';
|
||||
final wired = _isWiredExpression(expr);
|
||||
final upstreamType = _outputsEndpointType(field, graph);
|
||||
final accent = upstreamType != null
|
||||
? _typeAccent(upstreamType, Theme.of(context))
|
||||
: Theme.of(context).colorScheme.primary;
|
||||
yield _portDot(
|
||||
portKey: 'outputs:$field',
|
||||
center: p,
|
||||
isSource: false,
|
||||
connected: wired,
|
||||
accent: Theme.of(context).colorScheme.primary,
|
||||
accent: accent,
|
||||
onContextMenu: !wired
|
||||
? null
|
||||
: (pos) => _disconnectOutputPort(field, pos),
|
||||
|
|
@ -1592,6 +1632,30 @@ class _FlowCanvasState extends State<FlowCanvas>
|
|||
}
|
||||
}
|
||||
|
||||
/// Resolve the upstream type for an outputs-endpoint slot.
|
||||
/// Walks the edges into the outputs endpoint; when the
|
||||
/// source is a step whose ModuleSpec exposes the field's
|
||||
/// type, returns it. When source is an inputs endpoint
|
||||
/// (pass-through), returns the FlowInput's declared type.
|
||||
String? _outputsEndpointType(String field, FlowGraph graph) {
|
||||
for (final edge in graph.edges) {
|
||||
if (edge.toKind != EdgeEndpointKind.outputs) continue;
|
||||
if (edge.toField != field) continue;
|
||||
if (edge.fromKind == EdgeEndpointKind.inputs) {
|
||||
return graph.inputs[edge.fromField]?.type;
|
||||
}
|
||||
if (edge.fromKind == EdgeEndpointKind.step) {
|
||||
final step = graph.steps.firstWhere(
|
||||
(s) => s.id == edge.fromId,
|
||||
orElse: () => const FlowStep(id: '__missing__', use: ''),
|
||||
);
|
||||
if (step.id == '__missing__') return null;
|
||||
return _stepOutputType(step, edge.fromField);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// True when [expression] looks like a `$src.field` ref
|
||||
/// (the canonical wired-up form). Literal text or empty
|
||||
/// values count as unwired.
|
||||
|
|
@ -1631,26 +1695,42 @@ class _FlowCanvasState extends State<FlowCanvas>
|
|||
return set;
|
||||
}
|
||||
|
||||
/// Datatype → port + wire accent. Inspired by LabVIEW's
|
||||
/// long-running convention (string → pink/magenta, cluster
|
||||
/// → brown, numeric → amber/orange) where each type carries
|
||||
/// a sticky colour you learn after one flow. Tuned so the
|
||||
/// five F∆I types stay distinguishable both at glance
|
||||
/// (saturation differences) and for colour-blind operators
|
||||
/// (different luminance, not only different hue).
|
||||
Color _typeAccent(String type, ThemeData theme) {
|
||||
// Five distinct hues for the five payload types F-Delta-I
|
||||
// flows declare today. Picked from a high-contrast set
|
||||
// that survives both light and dark themes; intentionally
|
||||
// NOT pulled exclusively from the theme's primary /
|
||||
// secondary / tertiary slots because those collide once
|
||||
// a custom theme plugin is active.
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
switch (type) {
|
||||
case 'text':
|
||||
return const Color(0xFF42A5F5); // blue
|
||||
case 'bytes':
|
||||
return const Color(0xFFFF7043); // deep orange
|
||||
// Magenta / pink — LabVIEW's string colour. Pops on
|
||||
// both light + dark surfaces; high saturation reads
|
||||
// as "text flows here".
|
||||
return isDark ? const Color(0xFFFF6FB5) : const Color(0xFFD81B60);
|
||||
case 'json':
|
||||
return const Color(0xFFAB47BC); // purple
|
||||
// Amber / orange — structured-data colour, evokes
|
||||
// LabVIEW's cluster brown. Reads as "compound payload".
|
||||
return isDark ? const Color(0xFFFFB74D) : const Color(0xFFEF6C00);
|
||||
case 'bytes':
|
||||
// Cyan / teal — raw binary. Distinct from string-pink
|
||||
// and from the green of "file reference" so the
|
||||
// operator never confuses "I'm sending raw bytes" with
|
||||
// "I'm sending a file handle".
|
||||
return isDark ? const Color(0xFF4DD0E1) : const Color(0xFF00838F);
|
||||
case 'file':
|
||||
return const Color(0xFF66BB6A); // green
|
||||
// Green — file reference / resource handle. LabVIEW
|
||||
// uses green for refnums; reads as "this is a pointer
|
||||
// to something on disk".
|
||||
return isDark ? const Color(0xFF81C784) : const Color(0xFF2E7D32);
|
||||
case 'number':
|
||||
case 'integer':
|
||||
return const Color(0xFFFFCA28); // amber
|
||||
return isDark ? const Color(0xFFFFD54F) : const Color(0xFFF9A825);
|
||||
default:
|
||||
// Unknown type — neutral, deliberately desaturated
|
||||
// so the operator notices "I haven't typed this".
|
||||
return theme.colorScheme.onSurfaceVariant;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,11 +101,7 @@ class NodeGeometry {
|
|||
/// last port row overflows.
|
||||
static double heightFor(int inputCount, [int outputCount = 0]) {
|
||||
final rows = inputCount > outputCount ? inputCount : outputCount;
|
||||
return headerHeight +
|
||||
bodyTopPad +
|
||||
rows * portRowHeight +
|
||||
bodyBottomPad +
|
||||
2;
|
||||
return headerHeight + bodyTopPad + rows * portRowHeight + bodyBottomPad + 2;
|
||||
}
|
||||
|
||||
/// Y offset (from the card's top edge) where the input
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue