Skip to content

Instantly share code, notes, and snippets.

@rodydavis
Last active January 3, 2024 01:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rodydavis/e7c6f50331ba934a1fbf37f2be31f2a5 to your computer and use it in GitHub Desktop.
Save rodydavis/e7c6f50331ba934a1fbf37f2be31f2a5 to your computer and use it in GitHub Desktop.
import 'package:flutter/material.dart';
import 'package:signals/signals_flutter.dart';
class Editor extends StatefulWidget {
const Editor({super.key});
@override
State<Editor> createState() => _EditorState();
}
class _EditorState extends State<Editor> {
final nodes = mapSignal<Node, Offset>({});
final selection = setSignal<Node>({});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Editor'),
centerTitle: false,
),
body: Watch.builder(
builder: (context) {
return Stack(
children: [
Positioned.fill(
child: SizedBox.expand(
child: CustomPaint(
painter: NodeEdges(nodes, selection),
child: CustomMultiChildLayout(
delegate: NodeLayout(nodes),
children: [
for (final node in nodes.keys)
LayoutId(
id: node.id,
child: Container(
decoration: BoxDecoration(
color: Colors.grey.shade300,
border: Border.all(
color: selection.contains(node)
? Colors.lightBlue
: Colors.grey,
),
borderRadius: BorderRadius.circular(10),
),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanUpdate: (details) {
nodes[node] = nodes[node]! + details.delta;
},
onTap: () {
if (selection.contains(node)) {
selection.remove(node);
} else {
selection.add(node);
}
},
child: Stack(
children: [
Positioned.fill(child: node.build()),
Positioned(
top: 5,
left: 5,
child: Text(node.name),
),
],
),
),
),
),
],
),
),
),
),
Positioned(
top: 10,
left: 10,
child: Text('Nodes: ${nodes.length}'),
),
Positioned(
bottom: 10,
left: 10,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
FilledButton(
child: const Text('Add Number'),
onPressed: () => nodes[NumberNode(0)] = Offset.zero,
),
if (selection.length > 1 &&
selection
.toList()
.every((e) => e is Node<dynamic, num>)) ...[
const SizedBox(height: 10),
FilledButton(
child: const Text('Create Reducer'),
onPressed: () {
nodes[ReduceNode(
Reducer.add,
selection.toList().cast<Node<dynamic, num>>(),
)] = Offset.zero;
selection.clear();
},
),
if (selection.length == 2) ...[
const SizedBox(height: 10),
FilledButton(
child: const Text('Create If'),
onPressed: () {
nodes[IfNode(
Operator.equalTo,
selection.first as Node<dynamic, num>,
selection.last as Node<dynamic, num>,
)] = Offset.zero;
selection.clear();
},
),
],
],
],
),
),
],
);
},
),
);
}
}
class NodeLayout extends MultiChildLayoutDelegate {
final Map<Node, Offset> nodes;
NodeLayout(this.nodes);
@override
void performLayout(Size size) {
for (final nodeEntry in nodes.entries) {
final nodeSize = BoxConstraints.tight(nodeEntry.key.size());
layoutChild(nodeEntry.key.id, nodeSize);
positionChild(nodeEntry.key.id, nodeEntry.value);
}
}
@override
bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate) {
return true;
}
}
class NodeEdges extends CustomPainter {
final Map<Node, Offset> nodes;
final Set<Node> selected;
NodeEdges(this.nodes, this.selected);
@override
void paint(Canvas canvas, Size size) {
for (final nodeEntry in nodes.entries) {
final source = nodeEntry.value;
final sourceSize = nodeEntry.key.size();
final sourceOffset = source.translate(
sourceSize.width / 2,
sourceSize.height / 2,
);
if (nodeEntry.key.inputs.isNotEmpty) {
final paint = Paint()
..color = selected.contains(nodeEntry.key) ? Colors.blue : Colors.grey
..strokeWidth = 1;
for (final input in nodeEntry.key.inputs) {
final target = nodes[input]!;
final targetSize = input.size();
final targetOffset = target.translate(
targetSize.width / 2,
targetSize.height / 2,
);
canvas.drawLine(sourceOffset, targetOffset, paint);
}
}
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
abstract class Node<I, O> {
final String name;
final List<Node<dynamic, I>> inputs;
int get id => output.globalId;
Node({
required this.name,
required this.inputs,
});
ReadonlySignal<O> get output;
Widget build();
Size size();
}
class ReduceNode<T extends num> extends Node<T, T> {
@override
late final Computed<T> output;
late final Signal<Reducer> current;
ReduceNode(Reducer currentReducer, List<Node<dynamic, T>> inputs)
: current = signal(currentReducer),
super(name: 'Reducer', inputs: inputs) {
output = computed(() {
final source = inputs.map((e) => e.output()).toList();
return source.reduce((a, b) {
return switch (current.value) {
Reducer.add => a + b,
Reducer.subtract => a - b,
Reducer.divide => a / b,
Reducer.multiply => a * b,
} as T;
});
});
}
@override
Widget build() => Stack(
children: [
Positioned(
top: 5,
right: 5,
width: 80,
child: Column(
children: [
for (var i = 0; i < inputs.length; i++)
Row(
children: [
if (i == inputs.length - 1)
Text(
switch (current.value) {
Reducer.add => '+ ',
Reducer.subtract => '- ',
Reducer.divide => '/ ',
Reducer.multiply => '* ',
},
textAlign: TextAlign.left,
),
Expanded(
child: Text(
inputs[i].output.toString(),
textAlign: TextAlign.right,
),
),
],
),
const Divider(),
Row(
children: [
Expanded(
child: Text(
output.toString(),
textAlign: TextAlign.right,
),
),
],
),
],
),
),
Positioned(
bottom: 5,
right: 5,
left: 5,
child: DropdownButton(
isDense: true,
items: Reducer.values
.map((e) => DropdownMenuItem(
value: e,
child: Text(e.name),
))
.toList(),
value: current.value,
onChanged: (val) => current.set(val!),
),
),
],
);
@override
Size size() => Size(200, 40 + (20.0 * (inputs.length + 2)));
}
enum Reducer {
add,
subtract,
divide,
multiply,
}
class IfNode<T extends num> extends Node<T, bool> {
@override
late final Computed<bool> output;
late final Signal<Operator> current;
IfNode(Operator operatorCompare, Node<dynamic, T> a, Node<dynamic, T> b)
: current = signal(operatorCompare),
super(
name: 'If Node',
inputs: [a, b],
) {
output = computed(() {
final aVal = a.output();
final bVal = b.output();
return switch (current.value) {
Operator.greaterThan => aVal > bVal,
Operator.greaterThanEqualTo => aVal >= bVal,
Operator.lessThan => aVal < bVal,
Operator.lessThanEqualTo => aVal <= bVal,
Operator.equalTo => aVal == bVal,
Operator.notEqualTo => aVal != bVal,
};
});
}
@override
Widget build() => Stack(
children: [
Positioned(
top: 5,
right: 5,
child: Text('Result: $output'),
),
Positioned(
bottom: 5,
right: 5,
left: 5,
child: DropdownButton(
isDense: true,
items: Operator.values
.map((e) => DropdownMenuItem(
value: e,
child: Text(e.name),
))
.toList(),
value: current.value,
onChanged: (val) => current.set(val!),
),
),
],
);
@override
Size size() => const Size(200, 60);
}
enum Operator {
greaterThan,
greaterThanEqualTo,
lessThan,
lessThanEqualTo,
equalTo,
notEqualTo,
}
class NumberNode extends Node<void, num> {
@override
final Signal<num> output;
NumberNode(num value)
: output = signal(value),
super(
inputs: [],
name: 'Number',
);
@override
Widget build() => Stack(
children: [
Positioned(
bottom: 0,
left: 0,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
tooltip: 'Increment',
onPressed: () => output.value += 1,
icon: const Icon(Icons.add),
),
IconButton(
tooltip: 'Decrement',
onPressed: () => output.value -= 1,
icon: const Icon(Icons.remove),
),
],
),
),
Positioned(
top: 0,
right: 0,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
tooltip: 'Reset',
onPressed: () => output.value = output.initialValue,
icon: const Icon(Icons.restore),
),
IconButton(
tooltip: 'Undo',
onPressed: () => output.value = output.previousValue,
icon: const Icon(Icons.refresh),
),
],
),
),
Positioned(
bottom: 5,
right: 5,
child: Text('Value: $output'),
)
],
);
@override
Size size() => const Size(180, 60);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment