You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1428 lines
59 KiB
1428 lines
59 KiB
import 'dart:io'; |
|
import 'dart:math'; |
|
|
|
import 'package:file_picker/file_picker.dart'; |
|
import 'package:flutter/material.dart'; |
|
import 'package:flutter_hooks/flutter_hooks.dart'; |
|
import 'package:hetu_script/hetu_script.dart'; |
|
import 'package:hetu_script/values.dart'; |
|
import 'package:logic_circuits_simulator/components/visual_component.dart'; |
|
import 'package:logic_circuits_simulator/models.dart'; |
|
import 'package:logic_circuits_simulator/pages_arguments/design_component.dart'; |
|
import 'package:logic_circuits_simulator/state/component.dart'; |
|
import 'package:logic_circuits_simulator/state/project.dart'; |
|
import 'package:logic_circuits_simulator/state/projects.dart'; |
|
import 'package:logic_circuits_simulator/utils/future_call_debounce.dart'; |
|
import 'package:logic_circuits_simulator/utils/iterable_extension.dart'; |
|
import 'package:logic_circuits_simulator/utils/provider_hook.dart'; |
|
import 'package:logic_circuits_simulator/utils/stack_canvas_controller_hook.dart'; |
|
import 'package:provider/provider.dart'; |
|
import 'package:stack_canvas/stack_canvas.dart'; |
|
import 'package:tuple/tuple.dart'; |
|
import 'package:uuid/uuid.dart'; |
|
|
|
Key canvasKey = GlobalKey(); |
|
Key pickerKey = GlobalKey(); |
|
|
|
class DesignComponentPage extends HookWidget { |
|
final ComponentEntry component; |
|
|
|
const DesignComponentPage({required this.component, super.key}); |
|
DesignComponentPage.fromArguments(DesignComponentPageArguments args, {super.key}) |
|
: component = args.component; |
|
|
|
static const String routeName = '/project/component/design'; |
|
|
|
@override |
|
Widget build(BuildContext context) { |
|
final componentState = useProvider<ComponentState>(); |
|
final canvasController = useStackCanvasController( |
|
offsetReference: Reference.Center, |
|
); |
|
|
|
// Simulation vars |
|
final isSimulating = useState(false); |
|
final simulatePartially = useState(false); |
|
|
|
useListenable(componentState.partialVisualSimulation!); |
|
|
|
// Scripting |
|
final scriptingEnvironment = useState<Hetu?>(null); |
|
final loadScript = useMemoized(() => (String script) { |
|
scriptingEnvironment.value = Hetu(); |
|
scriptingEnvironment.value!.init( |
|
externalFunctions: { |
|
'unload': ( |
|
HTEntity entity, { |
|
List<dynamic> positionalArgs = const [], |
|
Map<String, dynamic> namedArgs = const {}, |
|
List<HTType> typeArgs = const [], |
|
}) { |
|
scriptingEnvironment.value = null; |
|
}, |
|
'alert': ( |
|
HTEntity entity, { |
|
List<dynamic> positionalArgs = const [], |
|
Map<String, dynamic> namedArgs = const {}, |
|
List<HTType> typeArgs = const [], |
|
}) { |
|
final content = positionalArgs[0] as String; |
|
final title = positionalArgs[1] as String? ?? 'Script Alert'; |
|
return showDialog( |
|
context: context, |
|
builder: (context) { |
|
return AlertDialog( |
|
title: Text(title), |
|
content: Text(content), |
|
actions: [ |
|
TextButton( |
|
onPressed: () { |
|
Navigator.of(context).pop(); |
|
}, |
|
child: const Text('OK'), |
|
), |
|
], |
|
); |
|
}, |
|
); |
|
}, |
|
'snackBar': ( |
|
HTEntity entity, { |
|
List<dynamic> positionalArgs = const [], |
|
Map<String, dynamic> namedArgs = const {}, |
|
List<HTType> typeArgs = const [], |
|
}) { |
|
final content = positionalArgs[0] as String; |
|
final actionName = positionalArgs[1] as String?; |
|
final actionFunction = positionalArgs[2]; |
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar( |
|
content: Text(content), |
|
action: actionName == null ? null : SnackBarAction( |
|
label: actionName, |
|
onPressed: () { |
|
if (actionFunction is String) { |
|
scriptingEnvironment.value?.invoke(actionFunction); |
|
} |
|
else if (actionFunction is HTFunction && scriptingEnvironment.value != null) { |
|
actionFunction.call(); |
|
} |
|
}, |
|
), |
|
)); |
|
}, |
|
'setTimeout': ( |
|
HTEntity entity, { |
|
List<dynamic> positionalArgs = const [], |
|
Map<String, dynamic> namedArgs = const {}, |
|
List<HTType> typeArgs = const [], |
|
}) { |
|
final millis = positionalArgs[0] as int; |
|
final function = positionalArgs[1]; |
|
final pos = namedArgs['positionalArgs'] ?? []; |
|
final named = namedArgs['namedArgs'] ?? {}; |
|
Future.delayed(Duration(milliseconds: millis)) |
|
.then((_) { |
|
if (function is String) { |
|
scriptingEnvironment.value?.invoke(function, positionalArgs: pos, namedArgs: Map.castFrom(named)); |
|
} |
|
else if (function is HTFunction && scriptingEnvironment.value != null) { |
|
function.call(positionalArgs: pos, namedArgs: Map.castFrom(named)); |
|
} |
|
}); |
|
}, |
|
'getInputs': ( |
|
HTEntity entity, { |
|
List<dynamic> positionalArgs = const [], |
|
Map<String, dynamic> namedArgs = const {}, |
|
List<HTType> typeArgs = const [], |
|
}) { |
|
return componentState.currentComponent!.inputs; |
|
}, |
|
'getOutputs': ( |
|
HTEntity entity, { |
|
List<dynamic> positionalArgs = const [], |
|
Map<String, dynamic> namedArgs = const {}, |
|
List<HTType> typeArgs = const [], |
|
}) { |
|
return componentState.currentComponent!.outputs; |
|
}, |
|
'simGetInputValues': ( |
|
HTEntity entity, { |
|
List<dynamic> positionalArgs = const [], |
|
Map<String, dynamic> namedArgs = const {}, |
|
List<HTType> typeArgs = const [], |
|
}) { |
|
return Map.of(componentState.partialVisualSimulation!.inputsValues); |
|
}, |
|
'simGetOutputValues': ( |
|
HTEntity entity, { |
|
List<dynamic> positionalArgs = const [], |
|
Map<String, dynamic> namedArgs = const {}, |
|
List<HTType> typeArgs = const [], |
|
}) { |
|
return Map.of(componentState.partialVisualSimulation!.outputsValues); |
|
}, |
|
'simSetInput': ( |
|
HTEntity entity, { |
|
List<dynamic> positionalArgs = const [], |
|
Map<String, dynamic> namedArgs = const {}, |
|
List<HTType> typeArgs = const [], |
|
}) { |
|
final inputName = positionalArgs[0] as String; |
|
final value = positionalArgs[1] as bool; |
|
|
|
return componentState.partialVisualSimulation!.modifyInput(inputName, value); |
|
}, |
|
'simSetInputs': ( |
|
HTEntity entity, { |
|
List<dynamic> positionalArgs = const [], |
|
Map<String, dynamic> namedArgs = const {}, |
|
List<HTType> typeArgs = const [], |
|
}) { |
|
final inputs = positionalArgs[0] as Map; |
|
|
|
return componentState.partialVisualSimulation!.provideInputs(inputs.map((key, value) => MapEntry(key as String, value as bool))); |
|
}, |
|
'simSetInputsBinary': ( |
|
HTEntity entity, { |
|
List<dynamic> positionalArgs = const [], |
|
Map<String, dynamic> namedArgs = const {}, |
|
List<HTType> typeArgs = const [], |
|
}) { |
|
final inputs = componentState.currentComponent!.inputs; |
|
final inputsNum = positionalArgs[0] as int; |
|
final inputsBinary = inputsNum.toRadixString(2).padLeft(inputs.length, '0'); |
|
final inputsMap = Map.fromIterables(inputs, inputsBinary.characters.map((c) => c == '1')); |
|
|
|
return componentState.partialVisualSimulation!.provideInputs(inputsMap); |
|
}, |
|
'simNextStep': ( |
|
HTEntity entity, { |
|
List<dynamic> positionalArgs = const [], |
|
Map<String, dynamic> namedArgs = const {}, |
|
List<HTType> typeArgs = const [], |
|
}) { |
|
return componentState.partialVisualSimulation!.nextStep(); |
|
}, |
|
'simRestart': ( |
|
HTEntity entity, { |
|
List<dynamic> positionalArgs = const [], |
|
Map<String, dynamic> namedArgs = const {}, |
|
List<HTType> typeArgs = const [], |
|
}) { |
|
return componentState.partialVisualSimulation!.restart(); |
|
}, |
|
'simIsPartiallySimulating': ( |
|
HTEntity entity, { |
|
List<dynamic> positionalArgs = const [], |
|
Map<String, dynamic> namedArgs = const {}, |
|
List<HTType> typeArgs = const [], |
|
}) { |
|
return simulatePartially.value; |
|
}, |
|
'simSetPartiallySimulating': ( |
|
HTEntity entity, { |
|
List<dynamic> positionalArgs = const [], |
|
Map<String, dynamic> namedArgs = const {}, |
|
List<HTType> typeArgs = const [], |
|
}) { |
|
simulatePartially.value = positionalArgs[0] as bool; |
|
}, |
|
}, |
|
); |
|
scriptingEnvironment.value!.eval(''' |
|
external fun unload |
|
external fun alert(message: String, [title]) |
|
external fun snackBar(message: String, [actionName, actionFunction]) |
|
external fun setTimeout(millis: int, function, {positionalArgs, namedArgs}) |
|
external fun getInputs -> List |
|
external fun getOutputs -> List |
|
external fun simGetInputValues -> Map |
|
external fun simGetOutputValues -> Map |
|
external fun simSetInput(inputName: String, value: bool) |
|
external fun simSetInputs(values: Map) |
|
external fun simSetInputsBinary(values: int) |
|
external fun simNextStep |
|
external fun simRestart |
|
external fun simIsPartiallySimulating -> bool |
|
external fun simSetPartiallySimulating(partiallySimulating: bool) |
|
'''); |
|
scriptingEnvironment.value!.eval(script, type: ResourceType.hetuModule); |
|
try { |
|
scriptingEnvironment.value!.invoke('onLoad'); |
|
} catch (e) { |
|
// onLoad handling is optional |
|
} |
|
try { |
|
scriptingEnvironment.value!.invoke('getFunctions'); |
|
} catch (e) { |
|
// Getting the callable functions of the script is mandatory |
|
showDialog( |
|
context: context, |
|
builder: (context) { |
|
return AlertDialog( |
|
title: const Text('Script Loading Failed'), |
|
content: const Text("The script doesn't implement the getFunctions function."), |
|
actions: [ |
|
TextButton( |
|
onPressed: () { |
|
Navigator.of(context).pop(); |
|
}, |
|
child: const Text('OK'), |
|
), |
|
], |
|
); |
|
}, |
|
); |
|
scriptingEnvironment.value = null; |
|
} |
|
}, [scriptingEnvironment.value]); |
|
|
|
// Design |
|
final movingWidgetUpdater = useState<void Function(double dx, double dy)?>(null); |
|
final movingWidget = useState<dynamic>(null); |
|
final deleteOnDrop = useState<bool>(false); |
|
final designSelection = useState<String?>(null); |
|
final wireToDelete = useState<String?>(null); |
|
final sourceToConnect = useState<String?>(null); |
|
final hoveredIO = useState<String?>(null); |
|
final deleteMovingWidget = useMemoized( |
|
() => () async { |
|
if (!deleteOnDrop.value) { |
|
return; |
|
} |
|
final w = movingWidget.value; |
|
final cs = componentState; |
|
// First remove all connected wires |
|
if (w is DesignComponent) { |
|
// Get project state to be able to remove dependency |
|
final projectState = Provider.of<ProjectState>(context, listen: false); |
|
|
|
final wires = cs.wiringDraft.wires |
|
.where( |
|
(wire) => wire.input.startsWith('${w.instanceId}/') || wire.output.startsWith('${w.instanceId}/') |
|
) |
|
.map((wire) => wire.wireId) |
|
.toList(); |
|
|
|
// Get component id before removing |
|
final componentId = cs.wiringDraft.instances.where((inst) => inst.instanceId == w.instanceId).first.componentId; |
|
|
|
await cs.updateDesign(cs.designDraft.copyWith( |
|
wires: cs.designDraft.wires |
|
.where((wire) => !wires.contains(wire.wireId)) |
|
.toList(), |
|
), commit: false); |
|
await cs.updateWiring(cs.wiringDraft.copyWith( |
|
wires: cs.wiringDraft.wires |
|
.where((wire) => !wires.contains(wire.wireId)) |
|
.toList(), |
|
), commit: false); |
|
await cs.updateDesign(cs.designDraft.copyWith( |
|
components: cs.designDraft.components |
|
.where((comp) => comp.instanceId != w.instanceId) |
|
.toList(), |
|
)); |
|
await cs.updateWiring(cs.wiringDraft.copyWith( |
|
instances: cs.wiringDraft.instances |
|
.where((comp) => comp.instanceId != w.instanceId) |
|
.toList(), |
|
)); |
|
|
|
// Remove dependency if it's the last of its kind |
|
if (!cs.wiringDraft.instances.map((inst) => inst.componentId).contains(componentId)) { |
|
componentState.removeDependency(componentId, modifyCurrentComponent: true); |
|
await projectState.editComponent(componentState.currentComponent!); |
|
} |
|
} |
|
else if (w is DesignInput) { |
|
final wires = cs.wiringDraft.wires |
|
.where( |
|
(wire) => wire.output == 'self/${w.name}', |
|
) |
|
.map((wire) => wire.wireId) |
|
.toList(); |
|
|
|
await cs.updateDesign(cs.designDraft.copyWith( |
|
wires: cs.designDraft.wires |
|
.where((wire) => !wires.contains(wire.wireId)) |
|
.toList(), |
|
), commit: false); |
|
await cs.updateWiring(cs.wiringDraft.copyWith( |
|
wires: cs.wiringDraft.wires |
|
.where((wire) => !wires.contains(wire.wireId)) |
|
.toList(), |
|
)); |
|
await cs.updateDesign(cs.designDraft.copyWith( |
|
inputs: cs.designDraft.inputs |
|
.where((input) => input.name != w.name) |
|
.toList(), |
|
)); |
|
} |
|
else if (w is DesignOutput) { |
|
final wires = cs.wiringDraft.wires |
|
.where( |
|
(wire) => wire.input == 'self/${w.name}', |
|
) |
|
.map((wire) => wire.wireId) |
|
.toList(); |
|
|
|
await cs.updateDesign(cs.designDraft.copyWith( |
|
wires: cs.designDraft.wires |
|
.where((wire) => !wires.contains(wire.wireId)) |
|
.toList(), |
|
), commit: false); |
|
await cs.updateWiring(cs.wiringDraft.copyWith( |
|
wires: cs.wiringDraft.wires |
|
.where((wire) => !wires.contains(wire.wireId)) |
|
.toList(), |
|
)); |
|
await cs.updateDesign(cs.designDraft.copyWith( |
|
outputs: cs.designDraft.outputs |
|
.where((output) => output.name != w.name) |
|
.toList(), |
|
)); |
|
} |
|
}, |
|
[ |
|
movingWidget.value, |
|
deleteOnDrop.value, |
|
componentState.wiringDraft, |
|
componentState.designDraft, |
|
], |
|
); |
|
|
|
final widgets = useMemoized(() => [ |
|
for (final subcomponent in componentState.designDraft.components) |
|
CanvasObject( |
|
dx: subcomponent.x, |
|
dy: subcomponent.y, |
|
width: VisualComponent.getNeededWidth( |
|
context, |
|
componentState |
|
.getMetaByInstance(subcomponent.instanceId) |
|
.item2 |
|
.inputs, |
|
componentState |
|
.getMetaByInstance(subcomponent.instanceId) |
|
.item2 |
|
.outputs, |
|
Theme.of(context).textTheme.bodyMedium, |
|
), |
|
height: max( |
|
componentState.getMetaByInstance(subcomponent.instanceId).item2.inputs.length, |
|
componentState.getMetaByInstance(subcomponent.instanceId).item2.outputs.length, |
|
) * 30 + 10, |
|
child: Listener( |
|
behavior: HitTestBehavior.translucent, |
|
onPointerDown: (event) { |
|
final debouncer = FutureCallDebounce<List<double>>( |
|
futureCall: (xyList) { |
|
final dx = xyList[0]; |
|
final dy = xyList[1]; |
|
return componentState.updateDesign(componentState.designDraft.copyWith( |
|
components: componentState.designDraft.components.map( |
|
(e) => e.instanceId == subcomponent.instanceId ? e.copyWith( |
|
x: subcomponent.x + dx, |
|
y: subcomponent.y + dy, |
|
) : e, |
|
).toList(), |
|
), commit: false); |
|
}, |
|
combiner: (oldParams, newParams) { |
|
return oldParams.zipWith([newParams], (deltas) => deltas.fold<double>(0.0, (prev, elem) => prev + elem)).toList(); |
|
}, |
|
); |
|
movingWidgetUpdater.value = (dx, dy) { |
|
debouncer.call([dx, dy]); |
|
}; |
|
movingWidget.value = subcomponent; |
|
}, |
|
onPointerUp: (event) { |
|
deleteMovingWidget(); |
|
componentState.updateDesign(componentState.designDraft); |
|
movingWidgetUpdater.value = null; |
|
movingWidget.value = null; |
|
}, |
|
child: MouseRegion( |
|
opaque: false, |
|
hitTestBehavior: HitTestBehavior.translucent, |
|
cursor: movingWidget.value == subcomponent ? SystemMouseCursors.move : MouseCursor.defer, |
|
child: VisualComponent( |
|
name: componentState.getMetaByInstance(subcomponent.instanceId).item2.componentName, |
|
inputs: componentState.getMetaByInstance(subcomponent.instanceId).item2.inputs, |
|
outputs: componentState.getMetaByInstance(subcomponent.instanceId).item2.outputs, |
|
inputColors: isSimulating.value ? { |
|
for (final input in componentState.getMetaByInstance(subcomponent.instanceId).item2.inputs) |
|
input: componentState.partialVisualSimulation!.inputsValues['${subcomponent.instanceId}/$input'] == true ? Colors.green |
|
: componentState.partialVisualSimulation!.inputsValues['${subcomponent.instanceId}/$input'] == false ? Colors.red |
|
: Colors.black, |
|
} : null, |
|
outputColors: isSimulating.value ? { |
|
for (final output in componentState.getMetaByInstance(subcomponent.instanceId).item2.outputs) |
|
output: componentState.partialVisualSimulation!.outputsValues['${subcomponent.instanceId}/$output'] == true ? Colors.green |
|
: componentState.partialVisualSimulation!.outputsValues['${subcomponent.instanceId}/$output'] == false ? Colors.red |
|
: Colors.black, |
|
} : null, |
|
isNextToSimulate: isSimulating.value && componentState.partialVisualSimulation!.nextToSimulate.contains(subcomponent.instanceId), |
|
onInputHovered: designSelection.value == 'wiring' && sourceToConnect.value != null ? (input) { |
|
hoveredIO.value = '${subcomponent.instanceId}/$input'; |
|
} : null, |
|
onInputUnhovered: designSelection.value == 'wiring' && sourceToConnect.value != null ? (input) { |
|
hoveredIO.value = null; |
|
} : null, |
|
onOutputHovered: designSelection.value == 'wiring' && sourceToConnect.value == null ? (output) { |
|
hoveredIO.value = '${subcomponent.instanceId}/$output'; |
|
} : null, |
|
onOutputUnhovered: designSelection.value == 'wiring' && sourceToConnect.value == null ? (output) { |
|
hoveredIO.value = null; |
|
} : null, |
|
), |
|
), |
|
), |
|
), |
|
for (final input in componentState.designDraft.inputs) |
|
CanvasObject( |
|
dx: input.x, |
|
dy: input.y, |
|
width: IOComponent.getNeededWidth(context, input.name, textStyle: Theme.of(context).textTheme.bodyMedium), |
|
height: 40, |
|
child: Listener( |
|
behavior: HitTestBehavior.translucent, |
|
onPointerDown: (event) { |
|
final debouncer = FutureCallDebounce<List<double>>( |
|
futureCall: (xyList) { |
|
final dx = xyList[0]; |
|
final dy = xyList[1]; |
|
return componentState.updateDesign(componentState.designDraft.copyWith( |
|
inputs: componentState.designDraft.inputs.map( |
|
(e) => e.name == input.name ? e.copyWith( |
|
x: input.x + dx, |
|
y: input.y + dy, |
|
) : e, |
|
).toList(), |
|
), commit: false); |
|
}, |
|
combiner: (oldParams, newParams) { |
|
return oldParams.zipWith([newParams], (deltas) => deltas.fold<double>(0.0, (prev, elem) => prev + elem)).toList(); |
|
}, |
|
); |
|
movingWidgetUpdater.value = (dx, dy) { |
|
debouncer.call([dx, dy]); |
|
}; |
|
movingWidget.value = input; |
|
}, |
|
onPointerUp: (event) { |
|
deleteMovingWidget(); |
|
componentState.updateDesign(componentState.designDraft); |
|
movingWidgetUpdater.value = null; |
|
movingWidget.value = null; |
|
}, |
|
child: IOComponent( |
|
input: true, |
|
name: input.name, |
|
width: IOComponent.getNeededWidth(context, input.name, textStyle: Theme.of(context).textTheme.bodyMedium), |
|
color: isSimulating.value |
|
? (componentState.partialVisualSimulation!.outputsValues['self/${input.name}']! |
|
? Colors.green |
|
: Colors.red) |
|
: null, |
|
onTap: isSimulating.value ? () { |
|
componentState.partialVisualSimulation!.toggleInput(input.name); |
|
} : null, |
|
onHovered: designSelection.value == 'wiring' && sourceToConnect.value == null ? () { |
|
hoveredIO.value = 'self/${input.name}'; |
|
} : null, |
|
onUnhovered: designSelection.value == 'wiring' && sourceToConnect.value == null ? () { |
|
hoveredIO.value = null; |
|
} : null, |
|
flashing: designSelection.value == 'wiring' && sourceToConnect.value == null, |
|
), |
|
), |
|
), |
|
for (final output in componentState.designDraft.outputs) |
|
CanvasObject( |
|
dx: output.x, |
|
dy: output.y, |
|
width: IOComponent.getNeededWidth(context, output.name, textStyle: Theme.of(context).textTheme.bodyMedium), |
|
height: 40, |
|
child: Listener( |
|
behavior: HitTestBehavior.translucent, |
|
onPointerDown: (event) { |
|
final debouncer = FutureCallDebounce<List<double>>( |
|
futureCall: (xyList) { |
|
final dx = xyList[0]; |
|
final dy = xyList[1]; |
|
return componentState.updateDesign(componentState.designDraft.copyWith( |
|
outputs: componentState.designDraft.outputs.map( |
|
(e) => e.name == output.name ? e.copyWith( |
|
x: output.x + dx, |
|
y: output.y + dy, |
|
) : e, |
|
).toList(), |
|
), commit: false); |
|
}, |
|
combiner: (oldParams, newParams) { |
|
return oldParams.zipWith([newParams], (deltas) => deltas.fold<double>(0.0, (prev, elem) => prev + elem)).toList(); |
|
}, |
|
); |
|
movingWidgetUpdater.value = (dx, dy) { |
|
debouncer.call([dx, dy]); |
|
}; |
|
movingWidget.value = output; |
|
}, |
|
onPointerUp: (event) { |
|
deleteMovingWidget(); |
|
componentState.updateDesign(componentState.designDraft); |
|
movingWidgetUpdater.value = null; |
|
movingWidget.value = null; |
|
}, |
|
child: IOComponent( |
|
input: false, |
|
name: output.name, |
|
width: IOComponent.getNeededWidth(context, output.name, textStyle: Theme.of(context).textTheme.bodyMedium), |
|
color: isSimulating.value |
|
? (componentState.partialVisualSimulation!.inputsValues['self/${output.name}'] == true ? Colors.green |
|
: componentState.partialVisualSimulation!.inputsValues['self/${output.name}'] == false ? Colors.red |
|
: null) |
|
: null, |
|
onHovered: designSelection.value == 'wiring' && sourceToConnect.value != null ? () { |
|
hoveredIO.value = 'self/${output.name}'; |
|
} : null, |
|
onUnhovered: designSelection.value == 'wiring' && sourceToConnect.value != null ? () { |
|
hoveredIO.value = null; |
|
} : null, |
|
flashing: designSelection.value == 'wiring' && sourceToConnect.value != null, |
|
), |
|
), |
|
), |
|
for (final wire in componentState.wiringDraft.wires) |
|
(() { |
|
const ioCircleDiameter = 20; |
|
|
|
Offset from, to; |
|
if (wire.output.split('/')[0] == 'self') { |
|
// It's a component input |
|
|
|
// Find input |
|
final inputName = wire.output.split('/')[1]; |
|
final design = componentState.designDraft.inputs.where((i) => i.name == inputName).first; |
|
|
|
from = Offset( |
|
// Take into account widget length |
|
design.x + |
|
IOComponent.getNeededWidth( |
|
context, |
|
inputName, |
|
textStyle: Theme.of(context).textTheme.bodyMedium, |
|
), |
|
design.y + ioCircleDiameter + 1, |
|
); |
|
} |
|
else { |
|
// It's a subcomponent output |
|
|
|
// Find subcomponent |
|
final split = wire.output.split('/'); |
|
final subcomponentId = split[0]; |
|
final outputName = split[1]; |
|
final design = componentState.designDraft.components.where((c) => c.instanceId == subcomponentId).first; |
|
final subcomponent = componentState.getMetaByInstance(subcomponentId).item2; |
|
|
|
from = Offset( |
|
// Take into account widget length |
|
design.x + |
|
VisualComponent.getNeededWidth( |
|
context, |
|
subcomponent.inputs, |
|
subcomponent.outputs, |
|
Theme.of(context).textTheme.bodyMedium, |
|
), |
|
design.y + |
|
VisualComponent.getHeightOfIO( |
|
context, |
|
subcomponent.outputs, |
|
subcomponent.outputs.indexOf(outputName), |
|
Theme.of(context).textTheme.bodyMedium, |
|
), |
|
); |
|
} |
|
|
|
if (wire.input.split('/')[0] == 'self') { |
|
// It's a component output |
|
|
|
// Find output |
|
final outputName = wire.input.split('/')[1]; |
|
final design = componentState.designDraft.outputs.where((o) => o.name == outputName).first; |
|
|
|
to = Offset( |
|
design.x, |
|
design.y + ioCircleDiameter + 1, |
|
); |
|
} |
|
else { |
|
// It's a subcomponent input |
|
|
|
// Find subcomponent |
|
final split = wire.input.split('/'); |
|
final subcomponentId = split[0]; |
|
final inputName = split[1]; |
|
final design = componentState.designDraft.components.where((c) => c.instanceId == subcomponentId).first; |
|
final subcomponent = componentState.getMetaByInstance(subcomponentId).item2; |
|
|
|
to = Offset( |
|
// Take into account widget length |
|
design.x, |
|
design.y + |
|
VisualComponent.getHeightOfIO( |
|
context, |
|
subcomponent.inputs, |
|
subcomponent.inputs.indexOf(inputName), |
|
Theme.of(context).textTheme.bodyMedium, |
|
), |
|
); |
|
} |
|
|
|
var wireColor = Colors.black; |
|
if (isSimulating.value) { |
|
final wireValue = componentState.partialVisualSimulation!.outputsValues[wire.output]; |
|
if (wireValue == true) { |
|
wireColor = Colors.green; |
|
} |
|
else if (wireValue == false) { |
|
wireColor = Colors.red; |
|
} |
|
} |
|
|
|
return CanvasObject( |
|
dx: min(from.dx, to.dx), |
|
dy: min(from.dy, to.dy), |
|
width: (to - from).dx.abs(), |
|
height: (to - from).dy.abs(), |
|
child: MouseRegion( |
|
hitTestBehavior: HitTestBehavior.translucent, |
|
opaque: false, |
|
onEnter: (_) { |
|
if (designSelection.value == 'wiring') { |
|
wireToDelete.value = wire.wireId; |
|
} |
|
}, |
|
onExit: (_) { |
|
wireToDelete.value = null; |
|
}, |
|
child: GestureDetector( |
|
behavior: HitTestBehavior.translucent, |
|
onTap: () async { |
|
if (designSelection.value == 'wiring') { |
|
if (wireToDelete.value != wire.wireId) { |
|
wireToDelete.value = wire.wireId; |
|
} |
|
else { |
|
// Delete the wire |
|
await componentState.updateDesign(componentState.designDraft.copyWith( |
|
wires: componentState.designDraft.wires.where((w) => w.wireId != wireToDelete.value).toList(), |
|
)); |
|
await componentState.updateWiring(componentState.wiringDraft.copyWith( |
|
wires: componentState.wiringDraft.wires.where((w) => w.wireId != wireToDelete.value).toList(), |
|
)); |
|
wireToDelete.value = null; |
|
} |
|
} |
|
}, |
|
child: WireWidget( |
|
from: from, |
|
to: to, |
|
color: wireToDelete.value == wire.wireId ? Colors.red : wireColor, |
|
), |
|
), |
|
), |
|
); |
|
})(), |
|
], [componentState.wiringDraft, componentState.designDraft, isSimulating.value, componentState.partialVisualSimulation!.outputsValues, designSelection.value, sourceToConnect.value]); |
|
useEffect(() { |
|
final wList = widgets; |
|
canvasController.addCanvasObjects(wList); |
|
|
|
return () { |
|
// Cleanup |
|
for (final obj in wList) { |
|
canvasController.removeCanvasObject(obj); |
|
} |
|
}; |
|
}, [widgets]); |
|
useEffect(() { |
|
if (isSimulating.value && !simulatePartially.value && componentState.partialVisualSimulation!.nextToSimulate.isNotEmpty) { |
|
componentState.partialVisualSimulation!.nextStep(); |
|
} |
|
return null; |
|
}, [componentState.partialVisualSimulation!.outputsValues.entries.map((e) => '${e.key}:${e.value}').join(';'), simulatePartially.value, isSimulating.value]); |
|
|
|
return Scaffold( |
|
appBar: AppBar( |
|
centerTitle: true, |
|
title: Text('${isSimulating.value ? 'Simulation' : 'Design'} - ${component.componentName}'), |
|
actions: [ |
|
IconButton( |
|
icon: Icon(isSimulating.value ? Icons.stop : Icons.play_arrow), |
|
tooltip: isSimulating.value ? 'Stop Simulation' : 'Start Simulation', |
|
onPressed: () { |
|
isSimulating.value = !isSimulating.value; |
|
designSelection.value = null; |
|
}, |
|
), |
|
if (isSimulating.value) |
|
IconButton( |
|
icon: const Icon(Icons.description), |
|
tooltip: 'Scripting', |
|
onPressed: () { |
|
showDialog( |
|
context: context, |
|
builder: (context) { |
|
return AlertDialog( |
|
title: const Text('Scripting'), |
|
content: Column( |
|
mainAxisSize: MainAxisSize.min, |
|
children: [ |
|
ListTile( |
|
title: const Text('Load Script...'), |
|
onTap: () async { |
|
final nav = Navigator.of(context); |
|
|
|
final selectedFiles = await FilePicker.platform.pickFiles( |
|
dialogTitle: "Load Script", |
|
// allowedExtensions: ['ht', 'txt'], |
|
type: FileType.any, |
|
); |
|
if (selectedFiles == null || selectedFiles.files.isEmpty) { |
|
return; |
|
} |
|
|
|
try { |
|
final file = File(selectedFiles.files[0].path!); |
|
nav.pop(); |
|
loadScript(await file.readAsString()); |
|
} catch (e) { |
|
showDialog( |
|
context: context, |
|
builder: (context) { |
|
return AlertDialog( |
|
title: const Text('Script Loading Error'), |
|
content: Text(e.toString()), |
|
actions: [ |
|
TextButton( |
|
onPressed: () { |
|
Navigator.of(context).pop(); |
|
}, |
|
child: const Text('OK'), |
|
), |
|
], |
|
); |
|
}, |
|
); |
|
} |
|
}, |
|
), |
|
if (scriptingEnvironment.value != null) ...[ |
|
const Divider(), |
|
for (final function in scriptingEnvironment.value!.invoke('getFunctions')) |
|
ListTile( |
|
title: Text(function), |
|
onTap: () { |
|
Navigator.of(context).pop(); |
|
try { |
|
scriptingEnvironment.value!.invoke(function); |
|
} on HTError catch (e) { |
|
showDialog( |
|
context: context, |
|
builder: (context) { |
|
return AlertDialog( |
|
title: const Text('Script Error'), |
|
content: Text(e.toString()), |
|
actions: [ |
|
TextButton( |
|
onPressed: () => Navigator.of(context).pop(), |
|
child: const Text('OK'), |
|
), |
|
], |
|
); |
|
}, |
|
); |
|
scriptingEnvironment.value = null; |
|
} |
|
}, |
|
), |
|
], |
|
], |
|
), |
|
); |
|
}, |
|
); |
|
}, |
|
) |
|
], |
|
), |
|
body: OrientationBuilder( |
|
builder: (context, orientation) { |
|
final stackCanvas = GestureDetector( |
|
behavior: HitTestBehavior.translucent, |
|
onPanUpdate: (update) { |
|
final hw = movingWidgetUpdater.value; |
|
if (hw == null || isSimulating.value) { |
|
canvasController.offset = canvasController.offset.translate(update.delta.dx, update.delta.dy); |
|
} |
|
else { |
|
hw(update.delta.dx, update.delta.dy); |
|
} |
|
}, |
|
onTapUp: (update) async { |
|
final canvasCenterLocation = canvasController.canvasSize / 2; |
|
final canvasCenterLocationOffset = Offset(canvasCenterLocation.width, canvasCenterLocation.height); |
|
final canvasLocation = update.localPosition - canvasCenterLocationOffset + canvasController.offset; |
|
final ds = designSelection.value; |
|
|
|
if (ds == null) { |
|
return; |
|
} |
|
if (ds == 'wiring') { |
|
// Handle wire creation |
|
if (hoveredIO.value == null) { |
|
// If clicking on something not hovered, ignore |
|
return; |
|
} |
|
else if (sourceToConnect.value == null) { |
|
sourceToConnect.value = hoveredIO.value; |
|
hoveredIO.value = null; |
|
} else { |
|
// Create wire only if sink is not already connected |
|
if (componentState.wiringDraft.wires.where((w) => w.input == hoveredIO.value).isNotEmpty) { |
|
// Sink already connected |
|
final sinkType = hoveredIO.value!.startsWith('self/') ? 'component output' : 'subcomponent input'; |
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar( |
|
content: Text('Wire already connected to that $sinkType.'), |
|
)); |
|
return; |
|
} |
|
|
|
componentState.updateWiring(componentState.wiringDraft.copyWith( |
|
wires: componentState.wiringDraft.wires + [ |
|
WiringWire( |
|
wireId: const Uuid().v4(), |
|
output: sourceToConnect.value!, |
|
input: hoveredIO.value!, |
|
), |
|
], |
|
)); |
|
sourceToConnect.value = null; |
|
hoveredIO.value = null; |
|
} |
|
} |
|
else if (ds.startsWith('input:')) { |
|
final inputName = ds.substring(6); |
|
componentState.updateDesign(componentState.designDraft.copyWith( |
|
inputs: componentState.designDraft.inputs + [ |
|
DesignInput( |
|
name: inputName, |
|
x: canvasLocation.dx - IOComponent.getNeededWidth(context, inputName) / 2, |
|
y: canvasLocation.dy, |
|
), |
|
], |
|
)); |
|
designSelection.value = null; |
|
} |
|
else if (ds.startsWith('output:')) { |
|
final outputName = ds.substring(7); |
|
componentState.updateDesign(componentState.designDraft.copyWith( |
|
outputs: componentState.designDraft.outputs + [ |
|
DesignOutput( |
|
name: outputName, |
|
x: canvasLocation.dx - IOComponent.getNeededWidth(context, outputName) / 2, |
|
y: canvasLocation.dy, |
|
), |
|
], |
|
)); |
|
designSelection.value = null; |
|
} |
|
else { |
|
final currentProjectState = Provider.of<ProjectState>(context, listen: false); |
|
|
|
// Add subcomponent |
|
final splitted = ds.split('/'); |
|
var projectId = splitted[0]; |
|
final componentId = splitted[1]; |
|
|
|
if (Provider.of<ProjectState>(context, listen: false).currentProject!.projectId == projectId) { |
|
projectId = 'self'; |
|
} |
|
|
|
final depId = '$projectId/$componentId'; |
|
final project = projectId == 'self' |
|
? Provider.of<ProjectState>(context, listen: false).currentProject! |
|
: Provider.of<ProjectsState>(context, listen: false).index.projects.where((p) => p.projectId == projectId).first; |
|
final projectState = ProjectState(); |
|
await projectState.setCurrentProject(project); |
|
final component = projectState.index.components.where((c) => c.componentId == componentId).first; |
|
|
|
// Add dependency |
|
if (!componentState.hasDependency(depId)) { |
|
componentState.addDependency( |
|
depId, |
|
Tuple2( |
|
project, |
|
component, |
|
), |
|
modifyCurrentComponent: true, |
|
); |
|
await currentProjectState.editComponent(componentState.currentComponent!); |
|
} |
|
|
|
// Create component instance |
|
final instanceId = const Uuid().v4(); |
|
await componentState.updateWiring(componentState.wiringDraft.copyWith( |
|
instances: componentState.wiringDraft.instances + [ |
|
WiringInstance( |
|
componentId: depId, |
|
instanceId: instanceId, |
|
), |
|
], |
|
)); |
|
await componentState.updateDesign(componentState.designDraft.copyWith( |
|
components: componentState.designDraft.components + [ |
|
DesignComponent( |
|
instanceId: instanceId, |
|
x: canvasLocation.dx, |
|
y: canvasLocation.dy, |
|
), |
|
], |
|
)); |
|
|
|
// Recreate simulation with new subcomponent |
|
await componentState.recreatePartialSimulation(); |
|
|
|
designSelection.value = null; |
|
} |
|
}, |
|
child: Stack( |
|
children: [ |
|
StackCanvas( |
|
key: canvasKey, |
|
canvasController: canvasController, |
|
animationDuration: const Duration(milliseconds: 50), |
|
// disposeController: false, |
|
backgroundColor: Theme.of(context).colorScheme.background, |
|
), |
|
if (!isSimulating.value) |
|
Positioned( |
|
bottom: 0, |
|
right: 0, |
|
child: MouseRegion( |
|
hitTestBehavior: HitTestBehavior.translucent, |
|
opaque: false, |
|
onEnter: (_) { |
|
deleteOnDrop.value = true; |
|
}, |
|
onExit: (_) { |
|
deleteOnDrop.value = false; |
|
}, |
|
child: Padding( |
|
padding: const EdgeInsets.all(16.0), |
|
child: Icon( |
|
Icons.delete, |
|
color: movingWidget.value != null && deleteOnDrop.value ? Colors.red : null, |
|
), |
|
), |
|
), |
|
), |
|
], |
|
), |
|
); |
|
|
|
final debuggingButtons = DebuggingButtons( |
|
partialSimulation: simulatePartially.value, |
|
onPartialSimulationToggle: () { |
|
simulatePartially.value = !simulatePartially.value; |
|
}, |
|
onReset: simulatePartially.value ? () { |
|
componentState.partialVisualSimulation!.restart(); |
|
} : null, |
|
onNextStep: simulatePartially.value && componentState.partialVisualSimulation!.nextToSimulate.isNotEmpty ? () { |
|
componentState.partialVisualSimulation!.nextStep(); |
|
} : null, |
|
); |
|
|
|
final componentPicker = ComponentPicker( |
|
key: pickerKey, |
|
onSelectionUpdate: (selection) { |
|
designSelection.value = selection; |
|
if (selection != 'wiring') { |
|
wireToDelete.value = null; |
|
sourceToConnect.value = null; |
|
} |
|
}, |
|
selection: designSelection.value, |
|
); |
|
|
|
if (orientation == Orientation.portrait) { |
|
return Column( |
|
mainAxisSize: MainAxisSize.max, |
|
children: [ |
|
Expanded( |
|
child: Stack( |
|
children: [ |
|
stackCanvas, |
|
if (isSimulating.value) |
|
Positioned( |
|
top: 8, |
|
left: 0, |
|
right: 0, |
|
child: Center( |
|
child: debuggingButtons, |
|
), |
|
), |
|
], |
|
), |
|
), |
|
if (!isSimulating.value) |
|
componentPicker, |
|
], |
|
); |
|
} |
|
else { |
|
return Row( |
|
mainAxisSize: MainAxisSize.max, |
|
children: [ |
|
Expanded( |
|
child: Stack( |
|
children: [ |
|
stackCanvas, |
|
if (isSimulating.value) |
|
Positioned( |
|
top: 8, |
|
left: 0, |
|
right: 0, |
|
child: Center( |
|
child: debuggingButtons, |
|
), |
|
), |
|
], |
|
), |
|
), |
|
if (!isSimulating.value) |
|
componentPicker, |
|
], |
|
); |
|
} |
|
} |
|
), |
|
); |
|
} |
|
} |
|
|
|
class DebuggingButtons extends StatelessWidget { |
|
final bool partialSimulation; |
|
|
|
final void Function() onPartialSimulationToggle; |
|
final void Function()? onReset; |
|
final void Function()? onNextStep; |
|
|
|
const DebuggingButtons({super.key, required this.partialSimulation, required this.onPartialSimulationToggle, this.onReset, this.onNextStep}); |
|
|
|
@override |
|
Widget build(BuildContext context) { |
|
return Card( |
|
child: Padding( |
|
padding: const EdgeInsets.all(8.0), |
|
child: Row( |
|
mainAxisSize: MainAxisSize.min, |
|
children: [ |
|
IconButton( |
|
onPressed: onPartialSimulationToggle, |
|
icon: Icon( |
|
partialSimulation ? Icons.play_arrow : Icons.pause, |
|
), |
|
tooltip: partialSimulation ? 'Simulate to end' : 'Simulate partially', |
|
), |
|
IconButton( |
|
onPressed: onNextStep, |
|
icon: const Icon(Icons.navigate_next), |
|
tooltip: 'Simulate next step', |
|
), |
|
IconButton( |
|
onPressed: onReset, |
|
icon: const Icon(Icons.replay_outlined), |
|
tooltip: 'Restart simulation', |
|
), |
|
], |
|
), |
|
), |
|
); |
|
} |
|
} |
|
|
|
class ComponentPicker extends HookWidget { |
|
const ComponentPicker({required this.onSelectionUpdate, required this.selection, super.key}); |
|
|
|
final String? selection; |
|
final void Function(String? selection) onSelectionUpdate; |
|
|
|
@override |
|
Widget build(BuildContext context) { |
|
final projectsState = useProvider<ProjectsState>(); |
|
final tickerProvider = useSingleTickerProvider(); |
|
final tabBarControllerState = useState<TabController?>(null); |
|
useEffect(() { |
|
tabBarControllerState.value = TabController( |
|
length: 3 + projectsState.projects.length, |
|
vsync: tickerProvider, |
|
initialIndex: 1, |
|
); |
|
|
|
tabBarControllerState.value!.addListener(() { |
|
if (tabBarControllerState.value!.index == 0) { |
|
onSelectionUpdate('wiring'); |
|
} |
|
else { |
|
onSelectionUpdate(null); |
|
} |
|
}); |
|
|
|
return () { |
|
tabBarControllerState.value?.dispose(); |
|
}; |
|
}, []); |
|
final tabBarController = tabBarControllerState.value!; |
|
|
|
return OrientationBuilder( |
|
builder: (context, orientation) { |
|
return SizedBox( |
|
height: orientation == Orientation.portrait ? 200 : null, |
|
width: orientation == Orientation.landscape ? 300 : null, |
|
child: Column( |
|
mainAxisSize: MainAxisSize.min, |
|
children: [ |
|
TabBar( |
|
controller: tabBarController, |
|
tabs: [ |
|
const Tab( |
|
text: 'Wiring', |
|
), |
|
const Tab( |
|
text: 'Inputs', |
|
), |
|
const Tab( |
|
text: 'Outputs', |
|
), |
|
for (final project in projectsState.projects) |
|
Tab( |
|
text: project.projectName, |
|
), |
|
], |
|
isScrollable: true, |
|
), |
|
Expanded( |
|
child: TabBarView( |
|
controller: tabBarController, |
|
children: [ |
|
Center( |
|
child: Column( |
|
mainAxisSize: MainAxisSize.min, |
|
children: const [ |
|
Padding( |
|
padding: EdgeInsets.all(8.0), |
|
child: Text( |
|
'To create wires, click a source and then click a sink to link them.', |
|
), |
|
), |
|
Padding( |
|
padding: EdgeInsets.all(8.0), |
|
child: Text( |
|
'To remove wires, click them or tap them twice.', |
|
), |
|
), |
|
], |
|
), |
|
), |
|
IOComponentPickerOptions( |
|
orientation: orientation, |
|
outputs: false, |
|
selection: selection, |
|
onSelected: onSelectionUpdate, |
|
), |
|
IOComponentPickerOptions( |
|
orientation: orientation, |
|
outputs: true, |
|
selection: selection, |
|
onSelected: onSelectionUpdate, |
|
), |
|
for (final project in projectsState.projects) |
|
HookBuilder( |
|
builder: (context) { |
|
final scrollController = useScrollController(); |
|
|
|
final projectState = useFuture(() async { |
|
final projectState = ProjectState(); |
|
await projectState.setCurrentProject(project); |
|
return projectState; |
|
}()); |
|
|
|
if (projectState.data == null) { |
|
return Container(); |
|
} |
|
final components = projectState.data!.index.components; |
|
|
|
return Column( |
|
mainAxisSize: MainAxisSize.min, |
|
crossAxisAlignment: CrossAxisAlignment.stretch, |
|
children: [ |
|
const Padding( |
|
padding: EdgeInsets.all(8.0), |
|
child: Text('To add a component, select it below and then click on the canvas to place it.'), |
|
), |
|
Expanded( |
|
child: Scrollbar( |
|
controller: scrollController, |
|
scrollbarOrientation: orientation == Orientation.portrait ? ScrollbarOrientation.bottom : ScrollbarOrientation.right, |
|
child: SingleChildScrollView( |
|
controller: scrollController, |
|
scrollDirection: orientation == Orientation.portrait ? Axis.horizontal : Axis.vertical, |
|
child: Wrap( |
|
direction: orientation == Orientation.portrait ? Axis.vertical : Axis.horizontal, |
|
crossAxisAlignment: WrapCrossAlignment.center, |
|
children: [ |
|
for (final component in components) |
|
IntrinsicWidth( |
|
child: Card( |
|
color: selection == '${project.projectId}/${component.componentId}' ? Theme.of(context).colorScheme.primaryContainer : null, |
|
child: InkWell( |
|
onTap: () { |
|
if (selection != '${project.projectId}/${component.componentId}') { |
|
onSelectionUpdate('${project.projectId}/${component.componentId}'); |
|
} |
|
else { |
|
onSelectionUpdate(null); |
|
} |
|
}, |
|
child: Padding( |
|
padding: const EdgeInsets.all(8.0), |
|
child: Text( |
|
component.componentName, |
|
style: selection == '${project.projectId}/${component.componentId}' |
|
? TextStyle( |
|
inherit: true, |
|
color: Theme.of(context).colorScheme.onPrimaryContainer, |
|
) |
|
: null, |
|
), |
|
), |
|
), |
|
), |
|
), |
|
], |
|
), |
|
), |
|
), |
|
), |
|
], |
|
); |
|
} |
|
), |
|
], |
|
), |
|
), |
|
], |
|
), |
|
); |
|
}, |
|
); |
|
} |
|
} |
|
|
|
class IOComponentPickerOptions extends HookWidget { |
|
final Orientation orientation; |
|
final bool outputs; |
|
final String? selection; |
|
final void Function(String? selection) onSelected; |
|
|
|
const IOComponentPickerOptions({required this.orientation, required this.outputs, required this.selection, required this.onSelected, super.key,}); |
|
|
|
String getSelectionName(String option) => '${!outputs ? "input" : "output"}:$option'; |
|
|
|
@override |
|
Widget build(BuildContext context) { |
|
final componentState = useProvider<ComponentState>(); |
|
|
|
final scrollController = useScrollController(); |
|
|
|
final options = !outputs ? componentState.currentComponent!.inputs : componentState.currentComponent!.outputs; |
|
|
|
return Builder( |
|
builder: (context) { |
|
return Column( |
|
mainAxisSize: MainAxisSize.min, |
|
crossAxisAlignment: CrossAxisAlignment.stretch, |
|
children: [ |
|
Padding( |
|
padding: const EdgeInsets.all(8.0), |
|
child: Text('To add an ${!outputs ? "input" : "output"}, select it below and then click on the canvas to place it. You can only add one of each. Red ${!outputs ? "inputs" : "outputs"} have already been placed.'), |
|
), |
|
Expanded( |
|
child: Scrollbar( |
|
controller: scrollController, |
|
scrollbarOrientation: orientation == Orientation.portrait ? ScrollbarOrientation.bottom : ScrollbarOrientation.right, |
|
child: SingleChildScrollView( |
|
controller: scrollController, |
|
scrollDirection: orientation == Orientation.portrait ? Axis.horizontal : Axis.vertical, |
|
child: Wrap( |
|
direction: orientation == Orientation.portrait ? Axis.vertical : Axis.horizontal, |
|
crossAxisAlignment: WrapCrossAlignment.center, |
|
children: [ |
|
for (final option in options) |
|
IntrinsicWidth( |
|
child: Card( |
|
color: ( |
|
!outputs |
|
? componentState.designDraft.inputs.map((input) => input.name).contains(option) |
|
: componentState.designDraft.outputs.map((output) => output.name).contains(option) |
|
) |
|
? const Color.fromARGB(100, 255, 0, 0) |
|
: selection == getSelectionName(option) |
|
? Theme.of(context).colorScheme.primaryContainer |
|
: null, |
|
child: InkWell( |
|
onTap: ( |
|
!outputs |
|
? componentState.designDraft.inputs.map((input) => input.name).contains(option) |
|
: componentState.designDraft.outputs.map((output) => output.name).contains(option) |
|
) ? null : () { |
|
if (selection == getSelectionName(option)) { |
|
onSelected(null); |
|
} |
|
else { |
|
onSelected(getSelectionName(option)); |
|
} |
|
}, |
|
child: Padding( |
|
padding: const EdgeInsets.all(8.0), |
|
child: Text( |
|
option, |
|
style: selection == getSelectionName(option) |
|
? TextStyle( |
|
inherit: true, |
|
color: Theme.of(context).colorScheme.onPrimaryContainer, |
|
) |
|
: null, |
|
), |
|
), |
|
), |
|
), |
|
), |
|
], |
|
), |
|
), |
|
), |
|
), |
|
], |
|
); |
|
} |
|
); |
|
} |
|
}
|
|
|