diff --git a/lib/pages/design_component.dart b/lib/pages/design_component.dart index ef98627..6981a70 100644 --- a/lib/pages/design_component.dart +++ b/lib/pages/design_component.dart @@ -1,7 +1,11 @@ +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'; @@ -42,6 +46,239 @@ class DesignComponentPage extends HookWidget { useListenable(componentState.partialVisualSimulation!); + // Scripting + final scriptingEnvironment = useState(null); + final loadScript = useMemoized(() => (String script) { + scriptingEnvironment.value = Hetu(); + scriptingEnvironment.value!.init( + externalFunctions: { + 'unload': ( + HTEntity entity, { + List positionalArgs = const [], + Map namedArgs = const {}, + List typeArgs = const [], + }) { + scriptingEnvironment.value = null; + }, + 'alert': ( + HTEntity entity, { + List positionalArgs = const [], + Map namedArgs = const {}, + List 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 positionalArgs = const [], + Map namedArgs = const {}, + List 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 positionalArgs = const [], + Map namedArgs = const {}, + List 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 positionalArgs = const [], + Map namedArgs = const {}, + List typeArgs = const [], + }) { + return componentState.currentComponent!.inputs; + }, + 'getOutputs': ( + HTEntity entity, { + List positionalArgs = const [], + Map namedArgs = const {}, + List typeArgs = const [], + }) { + return componentState.currentComponent!.outputs; + }, + 'simGetInputValues': ( + HTEntity entity, { + List positionalArgs = const [], + Map namedArgs = const {}, + List typeArgs = const [], + }) { + return Map.of(componentState.partialVisualSimulation!.inputsValues); + }, + 'simGetOutputValues': ( + HTEntity entity, { + List positionalArgs = const [], + Map namedArgs = const {}, + List typeArgs = const [], + }) { + return Map.of(componentState.partialVisualSimulation!.outputsValues); + }, + 'simSetInput': ( + HTEntity entity, { + List positionalArgs = const [], + Map namedArgs = const {}, + List typeArgs = const [], + }) { + final inputName = positionalArgs[0] as String; + final value = positionalArgs[1] as bool; + + return componentState.partialVisualSimulation!.modifyInput(inputName, value); + }, + 'simSetInputs': ( + HTEntity entity, { + List positionalArgs = const [], + Map namedArgs = const {}, + List 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 positionalArgs = const [], + Map namedArgs = const {}, + List 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 positionalArgs = const [], + Map namedArgs = const {}, + List typeArgs = const [], + }) { + return componentState.partialVisualSimulation!.nextStep(); + }, + 'simRestart': ( + HTEntity entity, { + List positionalArgs = const [], + Map namedArgs = const {}, + List typeArgs = const [], + }) { + return componentState.partialVisualSimulation!.restart(); + }, + 'simIsPartiallySimulating': ( + HTEntity entity, { + List positionalArgs = const [], + Map namedArgs = const {}, + List typeArgs = const [], + }) { + return simulatePartially.value; + }, + 'simSetPartiallySimulating': ( + HTEntity entity, { + List positionalArgs = const [], + Map namedArgs = const {}, + List 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(null); final movingWidget = useState(null); final deleteOnDrop = useState(false); @@ -533,6 +770,95 @@ class DesignComponentPage extends HookWidget { 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!); + 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'), + ), + ], + ); + }, + ); + } + nav.pop(); + }, + ), + 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(