diff --git a/lib/models/component.dart b/lib/models/component.dart index 1dbb290..d749a32 100644 --- a/lib/models/component.dart +++ b/lib/models/component.dart @@ -20,6 +20,8 @@ class ComponentEntry with _$ComponentEntry { required bool visualDesigned, @JsonKey(defaultValue: []) required List dependencies, + @JsonKey(defaultValue: false) + required bool scriptBased, }) = _ComponentEntry; factory ComponentEntry.fromJson(Map json) => _$ComponentEntryFromJson(json); diff --git a/lib/models/component.freezed.dart b/lib/models/component.freezed.dart index 5a71c57..35426a5 100644 --- a/lib/models/component.freezed.dart +++ b/lib/models/component.freezed.dart @@ -34,6 +34,8 @@ mixin _$ComponentEntry { bool get visualDesigned => throw _privateConstructorUsedError; @JsonKey(defaultValue: []) List get dependencies => throw _privateConstructorUsedError; + @JsonKey(defaultValue: false) + bool get scriptBased => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -55,7 +57,8 @@ abstract class $ComponentEntryCopyWith<$Res> { @JsonKey(includeIfNull: false) List? truthTable, @JsonKey(includeIfNull: false) List? logicExpression, @JsonKey(defaultValue: false) bool visualDesigned, - @JsonKey(defaultValue: []) List dependencies}); + @JsonKey(defaultValue: []) List dependencies, + @JsonKey(defaultValue: false) bool scriptBased}); } /// @nodoc @@ -78,6 +81,7 @@ class _$ComponentEntryCopyWithImpl<$Res> Object? logicExpression = freezed, Object? visualDesigned = freezed, Object? dependencies = freezed, + Object? scriptBased = freezed, }) { return _then(_value.copyWith( componentId: componentId == freezed @@ -116,6 +120,10 @@ class _$ComponentEntryCopyWithImpl<$Res> ? _value.dependencies : dependencies // ignore: cast_nullable_to_non_nullable as List, + scriptBased: scriptBased == freezed + ? _value.scriptBased + : scriptBased // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -136,7 +144,8 @@ abstract class _$$_ComponentEntryCopyWith<$Res> @JsonKey(includeIfNull: false) List? truthTable, @JsonKey(includeIfNull: false) List? logicExpression, @JsonKey(defaultValue: false) bool visualDesigned, - @JsonKey(defaultValue: []) List dependencies}); + @JsonKey(defaultValue: []) List dependencies, + @JsonKey(defaultValue: false) bool scriptBased}); } /// @nodoc @@ -161,6 +170,7 @@ class __$$_ComponentEntryCopyWithImpl<$Res> Object? logicExpression = freezed, Object? visualDesigned = freezed, Object? dependencies = freezed, + Object? scriptBased = freezed, }) { return _then(_$_ComponentEntry( componentId: componentId == freezed @@ -199,6 +209,10 @@ class __$$_ComponentEntryCopyWithImpl<$Res> ? _value._dependencies : dependencies // ignore: cast_nullable_to_non_nullable as List, + scriptBased: scriptBased == freezed + ? _value.scriptBased + : scriptBased // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -215,7 +229,8 @@ class _$_ComponentEntry implements _ComponentEntry { @JsonKey(includeIfNull: false) final List? truthTable, @JsonKey(includeIfNull: false) final List? logicExpression, @JsonKey(defaultValue: false) required this.visualDesigned, - @JsonKey(defaultValue: []) required final List dependencies}) + @JsonKey(defaultValue: []) required final List dependencies, + @JsonKey(defaultValue: false) required this.scriptBased}) : _inputs = inputs, _outputs = outputs, _truthTable = truthTable, @@ -277,9 +292,13 @@ class _$_ComponentEntry implements _ComponentEntry { return EqualUnmodifiableListView(_dependencies); } + @override + @JsonKey(defaultValue: false) + final bool scriptBased; + @override String toString() { - return 'ComponentEntry(componentId: $componentId, componentName: $componentName, componentDescription: $componentDescription, inputs: $inputs, outputs: $outputs, truthTable: $truthTable, logicExpression: $logicExpression, visualDesigned: $visualDesigned, dependencies: $dependencies)'; + return 'ComponentEntry(componentId: $componentId, componentName: $componentName, componentDescription: $componentDescription, inputs: $inputs, outputs: $outputs, truthTable: $truthTable, logicExpression: $logicExpression, visualDesigned: $visualDesigned, dependencies: $dependencies, scriptBased: $scriptBased)'; } @override @@ -302,7 +321,9 @@ class _$_ComponentEntry implements _ComponentEntry { const DeepCollectionEquality() .equals(other.visualDesigned, visualDesigned) && const DeepCollectionEquality() - .equals(other._dependencies, _dependencies)); + .equals(other._dependencies, _dependencies) && + const DeepCollectionEquality() + .equals(other.scriptBased, scriptBased)); } @JsonKey(ignore: true) @@ -317,7 +338,8 @@ class _$_ComponentEntry implements _ComponentEntry { const DeepCollectionEquality().hash(_truthTable), const DeepCollectionEquality().hash(_logicExpression), const DeepCollectionEquality().hash(visualDesigned), - const DeepCollectionEquality().hash(_dependencies)); + const DeepCollectionEquality().hash(_dependencies), + const DeepCollectionEquality().hash(scriptBased)); @JsonKey(ignore: true) @override @@ -332,20 +354,17 @@ class _$_ComponentEntry implements _ComponentEntry { abstract class _ComponentEntry implements ComponentEntry { const factory _ComponentEntry( - {required final String componentId, - required final String componentName, - @JsonKey(includeIfNull: false) - final String? componentDescription, - required final List inputs, - required final List outputs, - @JsonKey(includeIfNull: false) - final List? truthTable, - @JsonKey(includeIfNull: false) - final List? logicExpression, - @JsonKey(defaultValue: false) - required final bool visualDesigned, - @JsonKey(defaultValue: []) - required final List dependencies}) = _$_ComponentEntry; + {required final String componentId, + required final String componentName, + @JsonKey(includeIfNull: false) final String? componentDescription, + required final List inputs, + required final List outputs, + @JsonKey(includeIfNull: false) final List? truthTable, + @JsonKey(includeIfNull: false) final List? logicExpression, + @JsonKey(defaultValue: false) required final bool visualDesigned, + @JsonKey(defaultValue: []) required final List dependencies, + @JsonKey(defaultValue: false) required final bool scriptBased}) = + _$_ComponentEntry; factory _ComponentEntry.fromJson(Map json) = _$_ComponentEntry.fromJson; @@ -374,6 +393,9 @@ abstract class _ComponentEntry implements ComponentEntry { @JsonKey(defaultValue: []) List get dependencies => throw _privateConstructorUsedError; @override + @JsonKey(defaultValue: false) + bool get scriptBased => throw _privateConstructorUsedError; + @override @JsonKey(ignore: true) _$$_ComponentEntryCopyWith<_$_ComponentEntry> get copyWith => throw _privateConstructorUsedError; diff --git a/lib/models/component.g.dart b/lib/models/component.g.dart index 8a81526..76cbd63 100644 --- a/lib/models/component.g.dart +++ b/lib/models/component.g.dart @@ -26,6 +26,7 @@ _$_ComponentEntry _$$_ComponentEntryFromJson(Map json) => ?.map((e) => e as String) .toList() ?? [], + scriptBased: json['scriptBased'] as bool? ?? false, ); Map _$$_ComponentEntryToJson(_$_ComponentEntry instance) { @@ -47,5 +48,6 @@ Map _$$_ComponentEntryToJson(_$_ComponentEntry instance) { writeNotNull('logicExpression', instance.logicExpression); val['visualDesigned'] = instance.visualDesigned; val['dependencies'] = instance.dependencies; + val['scriptBased'] = instance.scriptBased; return val; } diff --git a/lib/pages/edit_component.dart b/lib/pages/edit_component.dart index a471176..4d34c10 100644 --- a/lib/pages/edit_component.dart +++ b/lib/pages/edit_component.dart @@ -1,6 +1,8 @@ +import 'dart:io'; import 'dart:math'; import 'package:collection/collection.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:logic_circuits_simulator/components/logic_expression_field.dart'; @@ -14,10 +16,13 @@ import 'package:logic_circuits_simulator/pages_arguments/edit_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/state/script.dart'; import 'package:logic_circuits_simulator/utils/iterable_extension.dart'; import 'package:logic_circuits_simulator/utils/logic_expressions.dart'; import 'package:logic_circuits_simulator/utils/logic_operators.dart'; import 'package:logic_circuits_simulator/utils/provider_hook.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; @@ -44,6 +49,7 @@ class EditComponentPage extends HookWidget { : List.generate(logicExpressions.value!.length, (index) => null), ); final visualDesigned = useState(ce().visualDesigned); + final scriptBased = useState(ce().scriptBased); final inputs = useState(ce().inputs.toList()); final outputs = useState(ce().outputs.toList()); final componentNameEditingController = useTextEditingController(text: ce().componentName); @@ -64,7 +70,7 @@ class EditComponentPage extends HookWidget { // Don't allow saving empty outputs return false; } - if (truthTable.value == null && logicExpressions.value == null && !visualDesigned.value) { + if (truthTable.value == null && logicExpressions.value == null && !visualDesigned.value && !scriptBased.value) { // Don't allow saving components without functionality return false; } @@ -91,6 +97,9 @@ class EditComponentPage extends HookWidget { if (visualDesigned.value != ce().visualDesigned) { return true; } + if (scriptBased.value != ce().scriptBased) { + return true; + } return false; }, [ @@ -107,6 +116,8 @@ class EditComponentPage extends HookWidget { visualDesigned.value, ce().visualDesigned, logicExpressionsParsed.value, + ce().scriptBased, + scriptBased.value, ], ); @@ -413,7 +424,7 @@ class EditComponentPage extends HookWidget { textAlign: TextAlign.center, ), ) - else if (truthTable.value == null && logicExpressions.value == null && !visualDesigned.value) ...[ + else if (truthTable.value == null && logicExpressions.value == null && !visualDesigned.value && !scriptBased.value) ...[ SliverToBoxAdapter( child: Column( children: [ @@ -462,11 +473,13 @@ class EditComponentPage extends HookWidget { child: const Text('Visual Designer'), ), ), - const Padding( - padding: EdgeInsets.all(8.0), + Padding( + padding: const EdgeInsets.all(8.0), child: OutlinedButton( - onPressed: null, - child: Text('Script'), + onPressed: () async { + scriptBased.value = true; + }, + child: const Text('Script'), ), ), ], @@ -579,7 +592,7 @@ class EditComponentPage extends HookWidget { try { await Provider.of(context, listen: false).setCurrentComponent( project: projectState.currentProject!, - component: component, + component: ce(), onDependencyNeeded: (String projectId, String componentId) async { if (projectId == 'self') { final maybeComponent = projectState.index.components.where((c) => c.componentId == componentId).firstOrNull; @@ -622,7 +635,95 @@ class EditComponentPage extends HookWidget { ), ], ), - ) + ), + ], + if (scriptBased.value) ...[ + SliverToBoxAdapter( + child: Column( + children: [ + Text( + "Script Component", + style: Theme.of(context).textTheme.headline4, + textAlign: TextAlign.center, + ), + HookBuilder( + builder: (context) { + final stateListenable = useState( + ScriptState( + project: projectState.currentProject!, + component: ce(), + ), + ); + final scriptState = useListenable(stateListenable.value); + + if (!scriptState.loaded) { + return const Padding( + padding: EdgeInsets.all(8.0), + child: CircularProgressIndicator(), + ); + } + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: ElevatedButton( + onPressed: () async { + final scaffoldMessenger = ScaffoldMessenger.of(context); + final picked = await FilePicker.platform.pickFiles( + dialogTitle: 'Select Script', + // allowedExtensions: ['ht', 'txt'], + allowMultiple: false, + type: FileType.any, + ); + if (picked == null) { + return; + } + final file = File(picked.files.first.path!); + if (!await file.exists()) { + scaffoldMessenger.showSnackBar( + const SnackBar( + content: Text('The selected file does not exist'), + ), + ); + return; + } + await scriptState.setScriptContents(await file.readAsString()); + }, + child: Text(scriptState.scriptExists ? 'Replace Script' : 'Select Script'), + ), + ), + ), + if (scriptState.scriptContent != null) + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + scriptState.scriptContent!, + softWrap: true, + style: TextStyle( + inherit: true, + fontFamily: 'JetBrains Mono', + fontFamilyFallback: [ + 'JetBrains Mono', + 'Ubuntu Mono', + 'Menlo', + 'Cascadia Code', + 'Courier New', + 'Courier', + ], + ), + ), + ), + ], + ); + }, + ), + ], + ), + ), ], const SliverPadding( padding: EdgeInsets.only(bottom: 56 + 16 + 16), @@ -640,9 +741,9 @@ class EditComponentPage extends HookWidget { truthTable: truthTable.value, logicExpression: logicExpressions.value, visualDesigned: visualDesigned.value, + scriptBased: scriptBased.value, )); anySave.value = true; - // TODO: Implement saving }, tooltip: 'Save Component', child: const Icon(Icons.save), diff --git a/lib/state/project.dart b/lib/state/project.dart index 9655145..c29ff85 100644 --- a/lib/state/project.dart +++ b/lib/state/project.dart @@ -77,6 +77,7 @@ class ProjectState extends ChangeNotifier { outputs: [], visualDesigned: false, dependencies: [], + scriptBased: false, ); await _updateIndex(index.copyWith(components: index.components + [newComponent])); return newComponent; diff --git a/lib/state/script.dart b/lib/state/script.dart new file mode 100644 index 0000000..38c8252 --- /dev/null +++ b/lib/state/script.dart @@ -0,0 +1,58 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:logic_circuits_simulator/models.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; + +class ScriptState extends ChangeNotifier { + bool? _scriptExists; + String? _scriptContent; + + bool get loaded => _scriptExists != null; + bool get scriptExists => _scriptExists ?? false; + String? get scriptContent => _scriptContent; + + final ProjectEntry project; + final ComponentEntry component; + + ScriptState({required this.project, required this.component, bool invokeInit = true}) { + if (invokeInit) { + init(); + } + } + + Future _getScriptFile() async { + final appDir = await getApplicationDocumentsDirectory(); + final componentDir = Directory(path.join(appDir.path, 'LogicCircuitsSimulator', 'projects', project.projectId, 'components', component.componentId)); + if (!await componentDir.exists()) { + await componentDir.create(recursive: true); + } + return File(path.join(componentDir.path, 'script.ht')); + } + + Future init() async { + final scriptFile = await _getScriptFile(); + _scriptExists = await scriptFile.exists(); + if (scriptExists) { + _scriptContent = await scriptFile.readAsString(); + } + notifyListeners(); + } + + Future setScriptContents(String newContents) async { + final scriptFile = await _getScriptFile(); + await scriptFile.writeAsString(newContents); + _scriptContent = newContents; + _scriptExists = true; + notifyListeners(); + } + + Future deleteScript() async { + final scriptFile = await _getScriptFile(); + await scriptFile.delete(); + _scriptContent = null; + _scriptExists = false; + notifyListeners(); + } +} diff --git a/lib/utils/simulation.dart b/lib/utils/simulation.dart index 6a36fb6..73752f8 100644 --- a/lib/utils/simulation.dart +++ b/lib/utils/simulation.dart @@ -1,7 +1,9 @@ import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; +import 'package:hetu_script/hetu_script.dart'; import 'package:logic_circuits_simulator/models.dart'; import 'package:logic_circuits_simulator/state/component.dart'; +import 'package:logic_circuits_simulator/state/script.dart'; import 'package:logic_circuits_simulator/utils/iterable_extension.dart'; import 'package:logic_circuits_simulator/utils/logic_expressions.dart'; @@ -55,6 +57,23 @@ class SimulatedComponent { }, ); return {for (final it in results) it[0] as String: it[1] as bool}; + } else if (component.scriptBased) { + final state = ScriptState( + component: component, + project: project, + invokeInit: false, + ); + await state.init(); + if (!state.scriptExists) { + throw Exception('Script for component ${project.projectId}/${component.componentId} does not exist'); + } + final hetu = Hetu(); + hetu.init(); + final result = hetu.eval(state.scriptContent!, invokeFunc: 'simulate', positionalArgs: [inputs]); + return { + for (final output in component.outputs) + output: result[output] + }; } else if (state == null) { throw Exception('Cannot simulate designed component without its state'); } else {