2026-01-25 18:20:28 +00:00
|
|
|
// dart
|
2025-09-10 15:03:21 +00:00
|
|
|
import 'package:flutter/material.dart';
|
2026-02-16 13:19:10 +00:00
|
|
|
import 'package:flutter_markdown_plus_latex/flutter_markdown_plus_latex.dart';
|
|
|
|
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
2026-02-07 15:16:00 +00:00
|
|
|
import 'package:markdown/markdown.dart' as markdown;
|
2025-09-10 15:03:21 +00:00
|
|
|
import '../formula_models.dart';
|
|
|
|
|
import '../formula_evaluator.dart';
|
|
|
|
|
import '../corpus.dart';
|
2026-02-09 15:57:53 +00:00
|
|
|
import '../error_handler.dart';
|
2025-09-10 15:03:21 +00:00
|
|
|
import 'unit_dropdown.dart';
|
2026-02-25 08:08:06 +00:00
|
|
|
import 'formula_editor.dart';
|
2025-09-10 15:03:21 +00:00
|
|
|
|
|
|
|
|
class FormulaScreen extends StatefulWidget {
|
|
|
|
|
final Formula formula;
|
2025-09-20 14:46:21 +00:00
|
|
|
final Corpus corpus;
|
2025-09-10 15:03:21 +00:00
|
|
|
|
2026-01-21 07:49:56 +00:00
|
|
|
const FormulaScreen({super.key, required this.formula, required this.corpus});
|
2025-09-10 15:03:21 +00:00
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<FormulaScreen> createState() => _FormulaScreenState();
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-13 14:31:26 +00:00
|
|
|
//// Start of D4rtEditingController class ////
|
|
|
|
|
class D4rtEditingController extends TextEditingController {
|
|
|
|
|
String? _lastError;
|
|
|
|
|
String? get lastError => _lastError;
|
2025-11-05 09:35:53 +00:00
|
|
|
FormulaResult? _lastValue;
|
2026-02-27 10:29:15 +00:00
|
|
|
final bool isString;
|
2025-10-13 14:31:26 +00:00
|
|
|
|
2026-02-27 10:29:15 +00:00
|
|
|
D4rtEditingController({super.text, this.isString = false});
|
2025-10-13 14:31:26 +00:00
|
|
|
|
|
|
|
|
bool validate() {
|
2026-02-27 10:29:15 +00:00
|
|
|
if( _validateAsNumberExpression(text) ){
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if( isString && _validateAsStringExpression(text) ){
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool _validateAsNumberExpression(String text){
|
|
|
|
|
return _validateAsD4rtExpression(text) && _lastValue is NumberResult;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool _validateAsD4rtExpression(String text){
|
2025-10-13 14:31:26 +00:00
|
|
|
try {
|
2025-10-16 17:28:17 +00:00
|
|
|
_lastValue = null;
|
2026-01-28 10:04:33 +00:00
|
|
|
if( text.trim().isEmpty ){
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2025-10-16 17:28:17 +00:00
|
|
|
_lastValue = FormulaEvaluator.evaluateExpression(text);
|
2025-10-13 14:31:26 +00:00
|
|
|
_lastError = null;
|
|
|
|
|
return true;
|
2025-11-05 09:35:53 +00:00
|
|
|
} catch (e, s) {
|
2026-02-27 10:29:15 +00:00
|
|
|
//errorHandler.notify(e, s);
|
2025-10-13 14:31:26 +00:00
|
|
|
_lastError = e.toString();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-27 10:29:15 +00:00
|
|
|
bool _validateAsStringExpression(String text){
|
|
|
|
|
if( _validateAsD4rtExpression(text) && _lastValue is StringResult ){
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if( _validateAsD4rtExpression('"' + text + '"') && _lastValue is StringResult ){
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if( _validateAsD4rtExpression("'" + text + "'") && _lastValue is StringResult ){
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-01-25 17:39:43 +00:00
|
|
|
FormulaResult? get d4rtValue => _lastValue;
|
2025-10-16 17:28:17 +00:00
|
|
|
|
2026-02-28 13:22:18 +00:00
|
|
|
@override
|
2025-10-13 14:31:26 +00:00
|
|
|
set text(String newText) {
|
|
|
|
|
super.text = newText;
|
|
|
|
|
validate();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void notifyListeners() {
|
|
|
|
|
validate();
|
|
|
|
|
super.notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
//// End of D4rtEditingController class ////
|
|
|
|
|
|
2025-09-10 15:03:21 +00:00
|
|
|
class _FormulaScreenState extends State<FormulaScreen> {
|
|
|
|
|
final _formKey = GlobalKey<FormState>();
|
2025-10-13 14:31:26 +00:00
|
|
|
final Map<String, D4rtEditingController> _inputControllers = {};
|
2025-09-10 15:03:21 +00:00
|
|
|
final Map<String, String?> _selectedUnits = {};
|
2026-01-25 18:20:28 +00:00
|
|
|
final Map<String, String?> _selectedValues = {}; // for string dropdowns
|
2025-09-10 15:03:21 +00:00
|
|
|
String? _result;
|
|
|
|
|
String? _selectedOutputUnit;
|
2026-02-11 07:45:56 +00:00
|
|
|
bool _isDescriptionExpanded = false; // Track description expansion state
|
2025-09-10 15:03:21 +00:00
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
2025-09-14 14:40:54 +00:00
|
|
|
// Initialize controllers and units with listeners
|
2025-09-10 15:03:21 +00:00
|
|
|
for (final input in widget.formula.input) {
|
2025-09-14 14:56:13 +00:00
|
|
|
_selectedUnits[input.name] = input.unit;
|
2026-01-25 18:20:28 +00:00
|
|
|
if (input.values != null && input.values!.isNotEmpty) {
|
|
|
|
|
// string/categorical variable -> use dropdown
|
|
|
|
|
_selectedValues[input.name] = input.values!.first;
|
|
|
|
|
} else {
|
|
|
|
|
// numeric variable -> use D4rtEditingController
|
2026-02-27 10:29:15 +00:00
|
|
|
_inputControllers[input.name] = D4rtEditingController(isString: input.unit == "string");
|
2026-01-25 18:20:28 +00:00
|
|
|
_inputControllers[input.name]!.addListener(_evaluateFormula);
|
|
|
|
|
}
|
2025-09-10 15:03:21 +00:00
|
|
|
}
|
2025-09-14 14:56:13 +00:00
|
|
|
_selectedOutputUnit = widget.formula.output.unit;
|
2025-09-10 15:03:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
2025-09-14 14:40:54 +00:00
|
|
|
// Clean up controllers and listeners
|
2025-09-10 15:03:21 +00:00
|
|
|
for (final controller in _inputControllers.values) {
|
2025-09-14 14:40:54 +00:00
|
|
|
controller.removeListener(_evaluateFormula);
|
2025-09-10 15:03:21 +00:00
|
|
|
controller.dispose();
|
|
|
|
|
}
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _evaluateFormula() {
|
|
|
|
|
try {
|
|
|
|
|
final inputValues = <String, dynamic>{};
|
|
|
|
|
for (final input in widget.formula.input) {
|
2026-01-25 18:20:28 +00:00
|
|
|
// string/categorical variable
|
|
|
|
|
if (input.values != null && input.values!.isNotEmpty) {
|
|
|
|
|
final selected = _selectedValues[input.name];
|
|
|
|
|
if (selected == null) {
|
|
|
|
|
_result = "";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
inputValues[input.name] = selected;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// numeric variable - must have controller
|
2025-10-16 17:28:17 +00:00
|
|
|
final controller = _inputControllers[input.name]!;
|
2026-01-25 18:20:28 +00:00
|
|
|
final val = controller.d4rtValue;
|
|
|
|
|
if (val == null) {
|
2026-01-21 07:49:56 +00:00
|
|
|
_result = "";
|
|
|
|
|
return;
|
2025-10-16 17:28:17 +00:00
|
|
|
}
|
2025-09-10 15:03:21 +00:00
|
|
|
|
2026-01-25 18:20:28 +00:00
|
|
|
dynamic convertedValue;
|
|
|
|
|
if (val is NumberResult) {
|
|
|
|
|
if (input.unit != null) {
|
|
|
|
|
convertedValue = widget.corpus.convert(
|
|
|
|
|
val.value,
|
|
|
|
|
_selectedUnits[input.name]!,
|
|
|
|
|
input.unit as String,
|
2026-01-21 07:49:56 +00:00
|
|
|
);
|
2026-01-25 18:20:28 +00:00
|
|
|
} else {
|
|
|
|
|
convertedValue = val.value;
|
|
|
|
|
}
|
|
|
|
|
} else if (val is StringResult) {
|
|
|
|
|
convertedValue = val.value;
|
|
|
|
|
} else {
|
|
|
|
|
throw FormulaEvaluationException(
|
|
|
|
|
"Field ${input.name} has unsupported type ${val.runtimeType}",
|
|
|
|
|
);
|
2025-11-05 09:35:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inputValues[input.name] = convertedValue;
|
2025-09-10 15:03:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final evaluator = FormulaEvaluator();
|
|
|
|
|
final result = evaluator.evaluate(widget.formula, inputValues);
|
|
|
|
|
|
|
|
|
|
// Convert output to selected unit if needed
|
2025-11-09 19:29:58 +00:00
|
|
|
String? unit = widget.formula.output.unit;
|
2026-01-31 18:53:12 +00:00
|
|
|
if (unit != null && result is Number) {
|
2026-01-25 18:20:28 +00:00
|
|
|
final converted = widget.corpus.convert(result, unit, _selectedOutputUnit!);
|
|
|
|
|
if (converted is num) {
|
|
|
|
|
_result = converted.toStringAsFixed(2);
|
|
|
|
|
} else {
|
|
|
|
|
_result = converted.toString();
|
|
|
|
|
}
|
2026-01-21 07:49:56 +00:00
|
|
|
} else {
|
2026-01-25 18:20:28 +00:00
|
|
|
_result = result?.toString();
|
2025-11-09 19:29:58 +00:00
|
|
|
}
|
2025-10-14 17:21:35 +00:00
|
|
|
|
2025-09-10 15:03:21 +00:00
|
|
|
setState(() {});
|
2025-09-10 15:03:32 +00:00
|
|
|
} catch (e, stack) {
|
2026-02-09 15:57:53 +00:00
|
|
|
errorHandler.notify(e, stack);
|
2025-09-10 15:03:21 +00:00
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
|
|
|
SnackBar(
|
2026-02-09 15:57:53 +00:00
|
|
|
content: Text('Error: ${e.toString()}'),
|
2025-09-10 15:03:21 +00:00
|
|
|
backgroundColor: Theme.of(context).colorScheme.error,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return Scaffold(
|
2026-02-25 08:08:06 +00:00
|
|
|
appBar: AppBar(
|
|
|
|
|
title: Text(widget.formula.name),
|
|
|
|
|
actions: [
|
|
|
|
|
IconButton(
|
|
|
|
|
icon: const Icon(Icons.edit),
|
|
|
|
|
onPressed: () {
|
|
|
|
|
Navigator.push(
|
|
|
|
|
context,
|
|
|
|
|
MaterialPageRoute(
|
|
|
|
|
builder: (context) => FormulaEditor(
|
|
|
|
|
formula: widget.formula,
|
|
|
|
|
corpus: widget.corpus,
|
2026-02-26 20:49:32 +00:00
|
|
|
onSave: (updatedFormula) {
|
|
|
|
|
// Refresh the screen after saving
|
|
|
|
|
setState(() {
|
|
|
|
|
// The corpus has been updated, refresh the displayed formula
|
|
|
|
|
});
|
|
|
|
|
},
|
2026-02-25 08:08:06 +00:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
tooltip: 'Edit Formula',
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
2025-09-10 15:03:21 +00:00
|
|
|
body: Form(
|
|
|
|
|
key: _formKey,
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: const EdgeInsets.all(16.0),
|
2025-09-14 14:40:54 +00:00
|
|
|
child: ListView(
|
2025-09-10 15:03:21 +00:00
|
|
|
children: [
|
2025-09-16 20:01:34 +00:00
|
|
|
_buildDescriptionSection(),
|
2025-09-14 14:40:54 +00:00
|
|
|
_buildInputSection(),
|
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
_buildOutputSection(),
|
2025-09-10 15:03:21 +00:00
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-16 20:01:34 +00:00
|
|
|
Widget _buildDescriptionSection() {
|
2026-01-21 07:49:56 +00:00
|
|
|
if (widget.formula.description == null ||
|
2025-09-16 20:01:34 +00:00
|
|
|
widget.formula.description!.isEmpty) {
|
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 16:10:47 +00:00
|
|
|
return Card(
|
|
|
|
|
margin: const EdgeInsets.only(bottom: 16),
|
|
|
|
|
child: ExpansionTile(
|
|
|
|
|
title: Text(
|
2025-09-16 20:01:34 +00:00
|
|
|
'Description',
|
2026-02-09 16:10:47 +00:00
|
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
|
|
|
fontWeight: FontWeight.bold,
|
2025-09-16 20:01:34 +00:00
|
|
|
),
|
2026-02-09 16:10:47 +00:00
|
|
|
),
|
|
|
|
|
initiallyExpanded: _isDescriptionExpanded,
|
|
|
|
|
onExpansionChanged: (bool expanded) {
|
|
|
|
|
setState(() {
|
|
|
|
|
_isDescriptionExpanded = expanded;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
children: [
|
|
|
|
|
Padding(
|
|
|
|
|
padding: const EdgeInsets.all(16.0),
|
|
|
|
|
child: Container(
|
|
|
|
|
padding: const EdgeInsets.all(8),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Theme.of(context).colorScheme.surfaceVariant,
|
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
|
),
|
2026-02-16 13:19:10 +00:00
|
|
|
child: Markdown(
|
2026-02-09 16:10:47 +00:00
|
|
|
data: widget.formula.description!,
|
|
|
|
|
shrinkWrap: true,
|
|
|
|
|
builders: {
|
|
|
|
|
'latex': LatexElementBuilder(),
|
|
|
|
|
},
|
|
|
|
|
extensionSet: markdown.ExtensionSet(
|
|
|
|
|
[LatexBlockSyntax()],
|
|
|
|
|
[LatexInlineSyntax()],
|
|
|
|
|
),
|
|
|
|
|
),
|
2026-02-07 15:16:00 +00:00
|
|
|
),
|
2025-09-16 20:01:34 +00:00
|
|
|
),
|
2026-02-09 16:10:47 +00:00
|
|
|
],
|
|
|
|
|
),
|
2025-09-16 20:01:34 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 15:03:21 +00:00
|
|
|
Widget _buildInputSection() {
|
|
|
|
|
return Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
'Input Variables',
|
2026-01-21 07:49:56 +00:00
|
|
|
style: Theme.of(
|
|
|
|
|
context,
|
|
|
|
|
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
2025-09-10 15:03:21 +00:00
|
|
|
),
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
...widget.formula.input.map((variable) => _buildVariableRow(variable)),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildOutputSection() {
|
|
|
|
|
return Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
'Result',
|
2026-01-21 07:49:56 +00:00
|
|
|
style: Theme.of(
|
|
|
|
|
context,
|
|
|
|
|
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
2025-09-10 15:03:21 +00:00
|
|
|
),
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
2026-02-07 11:39:26 +00:00
|
|
|
// Fixed width for field name
|
2025-09-10 15:03:21 +00:00
|
|
|
SizedBox(
|
2025-10-05 14:53:46 +00:00
|
|
|
width: 150,
|
2026-02-07 11:39:26 +00:00
|
|
|
child: Text(
|
|
|
|
|
widget.formula.output.name,
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 8), // Add some spacing
|
|
|
|
|
// Flexible space for result field
|
|
|
|
|
Expanded(
|
2025-09-10 15:03:21 +00:00
|
|
|
child: TextFormField(
|
|
|
|
|
readOnly: true,
|
2025-09-14 14:41:34 +00:00
|
|
|
enabled: false,
|
2025-09-10 15:03:21 +00:00
|
|
|
controller: TextEditingController(text: _result),
|
|
|
|
|
decoration: const InputDecoration(
|
|
|
|
|
border: UnderlineInputBorder(),
|
2025-09-14 14:41:34 +00:00
|
|
|
filled: true,
|
2025-09-10 15:03:21 +00:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
UnitDropdown(
|
|
|
|
|
corpus: widget.corpus,
|
|
|
|
|
variable: widget.formula.output,
|
|
|
|
|
selectedUnit: _selectedOutputUnit,
|
|
|
|
|
onUnitChanged: (unit) {
|
2026-01-28 13:06:57 +00:00
|
|
|
_selectedOutputUnit = unit;
|
|
|
|
|
_evaluateFormula();
|
2026-01-31 18:53:12 +00:00
|
|
|
print( "En output unit changed to $unit: $_result");
|
2025-09-10 15:03:21 +00:00
|
|
|
setState(() {
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildVariableRow(VariableSpec variable) {
|
2026-01-25 18:20:28 +00:00
|
|
|
final isCategorical = variable.values != null && variable.values!.isNotEmpty;
|
|
|
|
|
|
2025-09-10 15:03:21 +00:00
|
|
|
return Padding(
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
2026-02-07 11:39:26 +00:00
|
|
|
// Fixed width for field name
|
|
|
|
|
SizedBox(
|
|
|
|
|
width: 150,
|
|
|
|
|
child: Text(
|
|
|
|
|
variable.name,
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
2025-09-10 15:03:21 +00:00
|
|
|
),
|
2026-02-07 11:39:26 +00:00
|
|
|
),
|
|
|
|
|
const SizedBox(width: 8), // Add some spacing
|
|
|
|
|
// Flexible space for input field
|
|
|
|
|
Expanded(
|
|
|
|
|
child: isCategorical
|
|
|
|
|
? DropdownButtonFormField<String>(
|
|
|
|
|
value: _selectedValues[variable.name],
|
|
|
|
|
items: variable.values!
|
|
|
|
|
.map((v) => DropdownMenuItem<String>(value: v, child: Text(v)))
|
|
|
|
|
.toList(),
|
|
|
|
|
onChanged: (v) {
|
|
|
|
|
_selectedValues[variable.name] = v;
|
|
|
|
|
_evaluateFormula();
|
|
|
|
|
setState(() {
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
decoration: const InputDecoration(
|
|
|
|
|
border: UnderlineInputBorder(),
|
|
|
|
|
),
|
|
|
|
|
validator: (value) {
|
|
|
|
|
if (value == null || value.isEmpty) return 'Required';
|
|
|
|
|
return null;
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
: TextFormField(
|
|
|
|
|
controller: _inputControllers[variable.name],
|
|
|
|
|
keyboardType: TextInputType.number,
|
|
|
|
|
inputFormatters: [
|
|
|
|
|
//FilteringTextInputFormatter.allow(RegExp(r'[0-9\.\-]')),
|
|
|
|
|
],
|
|
|
|
|
decoration: const InputDecoration(
|
|
|
|
|
border: UnderlineInputBorder(),
|
|
|
|
|
),
|
|
|
|
|
autovalidateMode: AutovalidateMode.always,
|
|
|
|
|
validator: (value) {
|
|
|
|
|
if (value == null || value.isEmpty) {
|
|
|
|
|
return 'Required';
|
|
|
|
|
}
|
|
|
|
|
return _inputControllers[variable.name]!.lastError;
|
|
|
|
|
},
|
2026-01-25 18:20:28 +00:00
|
|
|
),
|
2026-02-07 11:39:26 +00:00
|
|
|
),
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
if (variable.unit != null)
|
|
|
|
|
UnitDropdown(
|
|
|
|
|
corpus: widget.corpus,
|
|
|
|
|
variable: variable,
|
|
|
|
|
selectedUnit: _selectedUnits[variable.name],
|
|
|
|
|
onUnitChanged: (unit) {
|
|
|
|
|
setState(() {
|
|
|
|
|
_selectedUnits[variable.name] = unit;
|
|
|
|
|
});
|
|
|
|
|
_evaluateFormula();
|
|
|
|
|
},
|
2026-01-25 18:20:28 +00:00
|
|
|
),
|
2025-09-10 15:03:21 +00:00
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-09-10 15:03:32 +00:00
|
|
|
}
|