From e46fec8d35c8e7d05d5b48421615208da7e45e60 Mon Sep 17 00:00:00 2001 From: rs333 <9875430+rs333@users.noreply.github.com> Date: Sat, 8 Mar 2025 15:22:33 -0500 Subject: [PATCH 1/2] Fixes labels and Android/Windows text rendering. - Fixed axis labels missing t's - Fixed colors for text on Android and Windows - Improved input validation handling - Removed some unnecessary comments --- lib/am_widget.dart | 89 ++++++++++++----------------- lib/range_text_input_formatter.dart | 29 ++++++++++ lib/sinusoid_widget.dart | 34 +++++++---- 3 files changed, 88 insertions(+), 64 deletions(-) create mode 100644 lib/range_text_input_formatter.dart diff --git a/lib/am_widget.dart b/lib/am_widget.dart index 560a409..f5b7f70 100644 --- a/lib/am_widget.dart +++ b/lib/am_widget.dart @@ -4,7 +4,6 @@ import 'package:amgraph/am_data.dart'; import 'package:amgraph/sinusoid_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_math_fork/flutter_math.dart'; -import 'package:google_fonts/google_fonts.dart'; const graphWidth = 1000.0; @@ -15,6 +14,15 @@ class AmWidget extends StatefulWidget { State createState() => _AmWidgetState(); } +double inputFilter(double val, double lowLimit, double highLimit) { + if (val > highLimit) { + val = highLimit; + } else if (val < lowLimit) { + val = lowLimit; + } + return val; +} + class _AmWidgetState extends State { late AmData data; static const _scaler = TextScaler.linear(1); @@ -26,6 +34,17 @@ class _AmWidgetState extends State { final _thetamcontroller = TextEditingController(); final _thetaccontroller = TextEditingController(); + @override + void dispose() { + _vmcontroller.dispose(); + _fmcontroller.dispose(); + _vccontroller.dispose(); + _fccontroller.dispose(); + _thetamcontroller.dispose(); + _thetaccontroller.dispose(); + super.dispose(); + } + double lowFreq = 0; double highFreq = 0; void updateFreqs() { @@ -45,13 +64,6 @@ class _AmWidgetState extends State { } catch (e) { return; } - if (val > 10) { - val = 10; - _vmcontroller.text = '10'; - } else if (val < -10) { - val = -10; - _vmcontroller.text = '-10'; - } setState(() { data.vM = val; }); @@ -64,13 +76,6 @@ class _AmWidgetState extends State { } catch (e) { return; } - if (val > 2000) { - val = 2000; - _fmcontroller.text = '2000'; - } else if (val < 0) { - val = 0; - _fmcontroller.text = '0'; - } setState(() { data.fM = val; updateFreqs(); @@ -84,13 +89,6 @@ class _AmWidgetState extends State { } catch (e) { return; } - if (val > 10) { - val = 10; - _vccontroller.text = '10'; - } else if (val < -10) { - val = -10; - _vccontroller.text = '-10'; - } setState(() { data.vC = val; }); @@ -103,13 +101,6 @@ class _AmWidgetState extends State { } catch (e) { return; } - if (val > 100000) { - val = 100000; - _fccontroller.text = '100000'; - } else if (val < 0) { - val = 0; - _fccontroller.text = '0'; - } setState(() { data.fC = val; updateFreqs(); @@ -123,10 +114,6 @@ class _AmWidgetState extends State { } catch (e) { return; } - if (val > 360 || val < -360) { - val = val % 360; - _thetaccontroller.text = '$val'; - } setState(() { data.thetaC = val; }); @@ -139,10 +126,6 @@ class _AmWidgetState extends State { } catch (e) { return; } - if (val > 360 || val < -360) { - val = val % 360; - _thetamcontroller.text = '$val'; - } setState(() { data.thetaM = val; }); @@ -162,6 +145,7 @@ class _AmWidgetState extends State { fController: _fmcontroller, thetaController: _thetamcontroller, textScaler: _scaler, + maxF: 2000, ), SinusoidWidget( data: data, @@ -208,7 +192,6 @@ class _AmPainter extends CustomPainter { double sampleRate = 44100.0; double points = size.width - 2 * border; double viewWidth = 0.01; - double samples = viewWidth * sampleRate; double stopT = viewWidth * points / (graphWidth - 2 * border); double minorTicPoints = (graphWidth - 2 * border); final thickLine = @@ -277,7 +260,7 @@ class _AmPainter extends CustomPainter { thickLine, thinLine, size, - 'v\u208d(t)=${data.vC} sin(2\u03c0${data.fC} + ${data.thetaC})', + 'v\u208d(t)=${data.vC} sin(2\u03c0${data.fC}t + ${data.thetaC})', stopT, sampleRate, minorTicPoints, @@ -290,7 +273,7 @@ class _AmPainter extends CustomPainter { thickLine, thinLine, size, - 'v\u2090\u2098(t)=${data.vC} sin(2\u03c0 ${data.fC} ) + ${data.vM / 2} sin(2\u03c0 ${data.fC - data.fM}t) + ${data.vM / 2} sin(2\u03c0 ${data.fC + data.fM}t)', + 'v\u2090\u2098(t)=${data.vC} sin(2\u03c0 ${data.fC}t) + ${data.vM / 2} sin(2\u03c0 ${data.fC - data.fM}t) + ${data.vM / 2} sin(2\u03c0 ${data.fC + data.fM}t)', stopT, sampleRate, minorTicPoints, @@ -298,12 +281,8 @@ class _AmPainter extends CustomPainter { maxVoltage: 11.0, ); // Draw Vam - //for (double i = border; i <= (size.width - border); i += 1 / expand) { for (double t = 0; t <= stopT; t += 1 / sampleRate) { double i = t * points / stopT + border; - // (i - border) / - // (100 * - // (size.width - 2 * border)); // Ensures 1 complete cycle at 100Hz double vm = data.vM * sin(2.0 * pi * data.fM * t + data.thetaM * pi / 180); double vc = sin(2.0 * pi * data.fC * t + data.thetaC * pi / 180); @@ -356,15 +335,16 @@ class _AmPainter extends CustomPainter { if ((counter % 10) == 0) { canvas.drawLine(p1, p2, thickLine); TextSpan span = TextSpan( - style: GoogleFonts.notoSansMath(fontStyle: FontStyle.italic), + style: TextStyle(color: Color.fromARGB(255, 0, 34, 91)), text: '${counter / 10}', ); TextPainter tp = TextPainter( text: span, textDirection: TextDirection.ltr, ); - tp.layout(minWidth: 0, maxWidth: size.width); - tp.paint(canvas, Offset(i - 5, pos + 10 * 11)); + tp.layout(minWidth: 30, maxWidth: size.width); + tp.paint(canvas, Offset(i - 10, pos + 10 * 11)); + tp.dispose(); } else { canvas.drawLine(p1, p2, thinLine); } @@ -376,16 +356,16 @@ class _AmPainter extends CustomPainter { if ((i % 50) == 0) { canvas.drawLine(p1, p2, thickLine); TextSpan span = TextSpan( - style: GoogleFonts.notoSansMath(fontStyle: FontStyle.italic), - text: '${i / 10}', + style: TextStyle(color: Color.fromARGB(255, 0, 34, 91)), + text: '${i != 0 ? -i / 10 : 0}', ); TextPainter tp = TextPainter( text: span, textDirection: TextDirection.ltr, textAlign: TextAlign.right, ); - tp.layout(minWidth: 20, maxWidth: size.width); - tp.paint(canvas, Offset(border - 30, pos + i - 9)); + tp.layout(minWidth: 30, maxWidth: size.width); + tp.paint(canvas, Offset(border - 35, pos + i - 9)); } else { canvas.drawLine(p1, p2, thinLine); } @@ -401,11 +381,14 @@ class _AmPainter extends CustomPainter { axis, ); TextSpan span = TextSpan( - style: GoogleFonts.notoSansMath(fontStyle: FontStyle.italic), + style: TextStyle( + fontStyle: FontStyle.italic, + color: Color.fromARGB(255, 0, 34, 91), + ), text: xlabel, ); TextPainter tp = TextPainter(text: span, textDirection: TextDirection.ltr); - tp.layout(minWidth: 0, maxWidth: size.width); + tp.layout(minWidth: 30, maxWidth: size.width); tp.paint(canvas, Offset(50, pos - 10 * 13)); } diff --git a/lib/range_text_input_formatter.dart b/lib/range_text_input_formatter.dart new file mode 100644 index 0000000..303038f --- /dev/null +++ b/lib/range_text_input_formatter.dart @@ -0,0 +1,29 @@ +import 'package:flutter/services.dart'; + +class RangeTextInputFormatter extends TextInputFormatter { + final double min; + final double max; + + RangeTextInputFormatter({required this.min, required this.max}); + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + if (newValue.text.isEmpty) { + return newValue; + } + + try { + final double value = double.parse(newValue.text); + if (value < min || value > max) { + return oldValue; + } + } catch (e) { + return oldValue; + } + + return newValue; + } +} diff --git a/lib/sinusoid_widget.dart b/lib/sinusoid_widget.dart index f919c93..659e41d 100644 --- a/lib/sinusoid_widget.dart +++ b/lib/sinusoid_widget.dart @@ -1,6 +1,6 @@ import 'package:amgraph/am_data.dart'; +import 'package:amgraph/range_text_input_formatter.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_math_fork/flutter_math.dart'; class SinusoidWidget extends StatelessWidget { static const _scaler = TextScaler.linear(1); @@ -14,6 +14,12 @@ class SinusoidWidget extends StatelessWidget { this.textScaler = _scaler, this.subscript = '\u2098', this.signalName = 'modulating', + this.minV = -10, + this.maxV = 10, + this.minF = 0, + this.maxF = 100000, + this.minTheta = -360, + this.maxTheta = 360, }); final TextEditingController vController; @@ -25,6 +31,13 @@ class SinusoidWidget extends StatelessWidget { final AmData data; + final double minV; + final double maxV; + final double minF; + final double maxF; + final double minTheta; + final double maxTheta; + @override Widget build(BuildContext context) { return Column( @@ -38,6 +51,9 @@ class SinusoidWidget extends StatelessWidget { fit: FlexFit.loose, child: TextField( controller: vController, + inputFormatters: [ + RangeTextInputFormatter(min: minV, max: maxV), + ], decoration: InputDecoration( floatingLabelAlignment: FloatingLabelAlignment.start, floatingLabelStyle: TextStyle(fontStyle: FontStyle.italic), @@ -54,6 +70,9 @@ class SinusoidWidget extends StatelessWidget { fit: FlexFit.loose, child: TextField( controller: fController, + inputFormatters: [ + RangeTextInputFormatter(min: minF, max: maxF), + ], decoration: InputDecoration( floatingLabelAlignment: FloatingLabelAlignment.start, floatingLabelStyle: TextStyle(fontStyle: FontStyle.italic), @@ -70,6 +89,9 @@ class SinusoidWidget extends StatelessWidget { fit: FlexFit.loose, child: TextField( controller: thetaController, + inputFormatters: [ + RangeTextInputFormatter(min: minTheta, max: maxTheta), + ], decoration: InputDecoration( floatingLabelAlignment: FloatingLabelAlignment.start, floatingLabelStyle: TextStyle(fontStyle: FontStyle.italic), @@ -85,16 +107,6 @@ class SinusoidWidget extends StatelessWidget { Flexible(fit: FlexFit.tight, child: Text('')), ], ), - // Row( - // spacing: _spacing, - // mainAxisAlignment: MainAxisAlignment.center, - // children: [ - // SelectableMath.tex( - // 'v_$subscript(t)=${vController.text} sin(2\\pi ${fController.text} t + ${thetaController.text} )', - // textScaleFactor: textScaler.scale(1.5), - // ), - // ], - // ), ], ); } From e2f2619cecb28e75e1a3f054bce83692d89edf14 Mon Sep 17 00:00:00 2001 From: rs333 <9875430+rs333@users.noreply.github.com> Date: Sat, 8 Mar 2025 16:41:23 -0500 Subject: [PATCH 2/2] Refactored location of text handlers for inputs. - Added a couple unit tests. - Relocated some widgets and util functions. - Added a build and test action. - Updated equation rendering. --- .github/workflows/main.yml | 39 ++++ lib/am_data.dart | 15 +- lib/main.dart | 2 +- .../range_text_input_formatter.dart | 2 +- lib/{ => widgets}/am_widget.dart | 199 ++++++------------ .../sinusoid_input_widget.dart} | 95 ++++++--- .../range_text_input_formatter_test.dart | 32 +++ test/widget_test.dart | 21 -- test/widgets/sinusoid_input_widget_test.dart | 24 +++ 9 files changed, 236 insertions(+), 193 deletions(-) create mode 100644 .github/workflows/main.yml rename lib/{ => utils}/range_text_input_formatter.dart (96%) rename lib/{ => widgets}/am_widget.dart (65%) rename lib/{sinusoid_widget.dart => widgets/sinusoid_input_widget.dart} (51%) create mode 100644 test/utils/range_text_input_formatter_test.dart create mode 100644 test/widgets/sinusoid_input_widget_test.dart diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..e00cb6f --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,39 @@ +name: "Build and Test" + +on: + pull_request: + branches: + - main + + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Java + uses: actions/setup-java@v2 + with: + distribution: 'zulu' + java-version: '11' + + - name: Set up Flutter + run: | + git clone https://github.com/flutter/flutter.git -b stable + echo "$GITHUB_WORKSPACE/flutter/bin" >> $GITHUB_PATH + + - name: Flutter doctor + run: flutter/bin/flutter doctor -v + + - name: Install dependencies + run: flutter/bin/flutter pub get + + - name: Run tests + run: flutter/bin/flutter test + \ No newline at end of file diff --git a/lib/am_data.dart b/lib/am_data.dart index ae9d4e6..dc0dc7a 100644 --- a/lib/am_data.dart +++ b/lib/am_data.dart @@ -1,11 +1,14 @@ +class Sinusoid { + double amplitude = 0; + double frequency = 0; + double phase = 0; + Sinusoid(this.amplitude, this.frequency, this.phase); +} + class AmData { - double vM = 5; - double vC = 5; - double fM = 100; - double fC = 4000; - double thetaM = 0; - double thetaC = 0; + Sinusoid modulator = Sinusoid(5, 100, 0); bool showModulator = true; + Sinusoid carrier = Sinusoid(5, 4000, 0); bool showCarrier = true; bool showAm = true; } diff --git a/lib/main.dart b/lib/main.dart index 6e6fa16..ac2ac4e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,4 @@ -import 'package:amgraph/am_widget.dart'; +import 'package:amgraph/widgets/am_widget.dart'; import 'package:flutter/material.dart'; void main() { diff --git a/lib/range_text_input_formatter.dart b/lib/utils/range_text_input_formatter.dart similarity index 96% rename from lib/range_text_input_formatter.dart rename to lib/utils/range_text_input_formatter.dart index 303038f..7e6d8f0 100644 --- a/lib/range_text_input_formatter.dart +++ b/lib/utils/range_text_input_formatter.dart @@ -12,7 +12,7 @@ class RangeTextInputFormatter extends TextInputFormatter { TextEditingValue newValue, ) { if (newValue.text.isEmpty) { - return newValue; + return oldValue; } try { diff --git a/lib/am_widget.dart b/lib/widgets/am_widget.dart similarity index 65% rename from lib/am_widget.dart rename to lib/widgets/am_widget.dart index f5b7f70..1b21074 100644 --- a/lib/am_widget.dart +++ b/lib/widgets/am_widget.dart @@ -1,7 +1,6 @@ import 'dart:math'; - import 'package:amgraph/am_data.dart'; -import 'package:amgraph/sinusoid_widget.dart'; +import 'package:amgraph/widgets/sinusoid_input_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_math_fork/flutter_math.dart'; @@ -14,122 +13,13 @@ class AmWidget extends StatefulWidget { State createState() => _AmWidgetState(); } -double inputFilter(double val, double lowLimit, double highLimit) { - if (val > highLimit) { - val = highLimit; - } else if (val < lowLimit) { - val = lowLimit; - } - return val; -} - class _AmWidgetState extends State { late AmData data; static const _scaler = TextScaler.linear(1); - final _vmcontroller = TextEditingController(); - final _fmcontroller = TextEditingController(); - final _vccontroller = TextEditingController(); - final _fccontroller = TextEditingController(); - final _thetamcontroller = TextEditingController(); - final _thetaccontroller = TextEditingController(); - - @override - void dispose() { - _vmcontroller.dispose(); - _fmcontroller.dispose(); - _vccontroller.dispose(); - _fccontroller.dispose(); - _thetamcontroller.dispose(); - _thetaccontroller.dispose(); - super.dispose(); - } - - double lowFreq = 0; - double highFreq = 0; - void updateFreqs() { - lowFreq = data.fC - data.fM; - highFreq = data.fC + data.fM; - } - @override void initState() { data = AmData(); - updateFreqs(); - _vmcontroller.text = '${data.vM}'; - _vmcontroller.addListener(() { - double val = 0; - try { - val = double.parse(_vmcontroller.text); - } catch (e) { - return; - } - setState(() { - data.vM = val; - }); - }); - _fmcontroller.text = '${data.fM}'; - _fmcontroller.addListener(() { - double val = 0; - try { - val = double.parse(_fmcontroller.text); - } catch (e) { - return; - } - setState(() { - data.fM = val; - updateFreqs(); - }); - }); - _vccontroller.text = '${data.vC}'; - _vccontroller.addListener(() { - double val = 0; - try { - val = double.parse(_vccontroller.text); - } catch (e) { - return; - } - setState(() { - data.vC = val; - }); - }); - _fccontroller.text = '${data.fC}'; - _fccontroller.addListener(() { - double val = 0; - try { - val = double.parse(_fccontroller.text); - } catch (e) { - return; - } - setState(() { - data.fC = val; - updateFreqs(); - }); - }); - _thetaccontroller.text = '${data.thetaC}'; - _thetaccontroller.addListener(() { - double val = 0; - try { - val = double.parse(_thetaccontroller.text); - } catch (e) { - return; - } - setState(() { - data.thetaC = val; - }); - }); - _thetamcontroller.text = '${data.thetaM}'; - _thetamcontroller.addListener(() { - double val = 0; - try { - val = double.parse(_thetamcontroller.text); - } catch (e) { - return; - } - setState(() { - data.thetaM = val; - }); - }); super.initState(); } @@ -139,24 +29,36 @@ class _AmWidgetState extends State { spacing: 5, children: [ Text(""), - SinusoidWidget( + SinusoidInputWidget( data: data, - vController: _vmcontroller, - fController: _fmcontroller, - thetaController: _thetamcontroller, + initialValue: data.modulator, + onChanged: (v, f, theta) { + setState(() { + data.modulator.amplitude = v; + data.modulator.frequency = f; + data.modulator.phase = theta; + }); + }, textScaler: _scaler, maxF: 2000, ), - SinusoidWidget( + SinusoidInputWidget( data: data, - vController: _vccontroller, - fController: _fccontroller, - thetaController: _thetaccontroller, + initialValue: data.carrier, + onChanged: (v, f, theta) { + setState(() { + data.carrier.amplitude = v; + data.carrier.frequency = f; + data.carrier.phase = theta; + }); + }, signalName: 'carrier', subscript: 'c', textScaler: _scaler, ), - Math.tex('m = \\frac{${data.vM}}{${data.vC}} = ${data.vM / data.vC}'), + Math.tex( + 'm = \\frac{${data.modulator.amplitude}}{${data.carrier.amplitude}} = ${data.modulator.amplitude / data.carrier.amplitude}', + ), Divider(), Expanded( child: SingleChildScrollView( @@ -170,6 +72,33 @@ class _AmWidgetState extends State { painter: _AmPainter(data), ), ), + Positioned( + top: 30, + left: 45, + child: SelectableMath.tex( + data.modulator.phase < 0 + ? 'v_m(t)=${data.modulator.amplitude} sin(2\u03c0${data.modulator.frequency} - ${-data.modulator.phase}°)' + : 'v_m(t)=${data.modulator.amplitude} sin(2\u03c0${data.modulator.frequency} + ${data.modulator.phase}°)', + ), + ), + Positioned( + top: 290, + left: 45, + child: SelectableMath.tex( + data.carrier.phase < 0 + ? 'v_c(t)=${data.carrier.amplitude} sin(2\u03c0${data.carrier.frequency}t - ${-data.carrier.phase}°)' + : 'v_c(t)=${data.carrier.amplitude} sin(2\u03c0${data.carrier.frequency}t + ${data.carrier.phase}°)', + ), + ), + Positioned( + top: 290 + 280, + left: 45, + child: SelectableMath.tex( + data.modulator.amplitude < 0 + ? 'v_{am}(t)=${data.carrier.amplitude} sin(2\u03c0 ${data.carrier.frequency}t) - ${-data.modulator.amplitude / 2} sin(2\u03c0 ${data.carrier.frequency - data.modulator.frequency}t) - ${-data.modulator.amplitude / 2} sin(2\u03c0 ${data.carrier.frequency + data.modulator.frequency}t)' + : 'v_{am}(t)=${data.carrier.amplitude} sin(2\u03c0 ${data.carrier.frequency}t) + ${data.modulator.amplitude / 2} sin(2\u03c0 ${data.carrier.frequency - data.modulator.frequency}t) + ${data.modulator.amplitude / 2} sin(2\u03c0 ${data.carrier.frequency + data.modulator.frequency}t)', + ), + ), ], ), ), @@ -238,7 +167,6 @@ class _AmPainter extends CustomPainter { thickLine, thinLine, size, - 'v\u2098(t)=${data.vM} sin(2\u03c0${data.fM} + ${data.thetaM})', stopT, sampleRate, minorTicPoints, @@ -260,7 +188,6 @@ class _AmPainter extends CustomPainter { thickLine, thinLine, size, - 'v\u208d(t)=${data.vC} sin(2\u03c0${data.fC}t + ${data.thetaC})', stopT, sampleRate, minorTicPoints, @@ -273,7 +200,6 @@ class _AmPainter extends CustomPainter { thickLine, thinLine, size, - 'v\u2090\u2098(t)=${data.vC} sin(2\u03c0 ${data.fC}t) + ${data.vM / 2} sin(2\u03c0 ${data.fC - data.fM}t) + ${data.vM / 2} sin(2\u03c0 ${data.fC + data.fM}t)', stopT, sampleRate, minorTicPoints, @@ -284,11 +210,17 @@ class _AmPainter extends CustomPainter { for (double t = 0; t <= stopT; t += 1 / sampleRate) { double i = t * points / stopT + border; double vm = - data.vM * sin(2.0 * pi * data.fM * t + data.thetaM * pi / 180); - double vc = sin(2.0 * pi * data.fC * t + data.thetaC * pi / 180); - double v = -10 * (data.vC + vm) * vc; + data.modulator.amplitude * + sin( + 2.0 * pi * data.modulator.frequency * t + + data.modulator.phase * pi / 180, + ); + double vc = sin( + 2.0 * pi * data.carrier.frequency * t + data.carrier.phase * pi / 180, + ); + double v = -10 * (data.carrier.amplitude + vm) * vc; var currm = Offset(i, -10 * vm + mPos); - var currc = Offset(i, -10 * data.vC * vc + cPos); + var currc = Offset(i, -10 * data.carrier.amplitude * vc + cPos); var curram = Offset(i, v + amPos); if (i == border) { lastm = currm; @@ -316,7 +248,6 @@ class _AmPainter extends CustomPainter { Paint thickLine, Paint thinLine, Size size, - String xlabel, double stopT, double sampleRate, double minorTicPoints, @@ -343,7 +274,7 @@ class _AmPainter extends CustomPainter { textDirection: TextDirection.ltr, ); tp.layout(minWidth: 30, maxWidth: size.width); - tp.paint(canvas, Offset(i - 10, pos + 10 * 11)); + tp.paint(canvas, Offset(i - 4, pos + 10 * 11)); tp.dispose(); } else { canvas.drawLine(p1, p2, thinLine); @@ -380,16 +311,6 @@ class _AmPainter extends CustomPainter { Offset(size.width - border, pos), axis, ); - TextSpan span = TextSpan( - style: TextStyle( - fontStyle: FontStyle.italic, - color: Color.fromARGB(255, 0, 34, 91), - ), - text: xlabel, - ); - TextPainter tp = TextPainter(text: span, textDirection: TextDirection.ltr); - tp.layout(minWidth: 30, maxWidth: size.width); - tp.paint(canvas, Offset(50, pos - 10 * 13)); } void drawFreqAxis( diff --git a/lib/sinusoid_widget.dart b/lib/widgets/sinusoid_input_widget.dart similarity index 51% rename from lib/sinusoid_widget.dart rename to lib/widgets/sinusoid_input_widget.dart index 659e41d..4d702d3 100644 --- a/lib/sinusoid_widget.dart +++ b/lib/widgets/sinusoid_input_widget.dart @@ -1,16 +1,21 @@ import 'package:amgraph/am_data.dart'; -import 'package:amgraph/range_text_input_formatter.dart'; +import 'package:amgraph/utils/range_text_input_formatter.dart'; import 'package:flutter/material.dart'; -class SinusoidWidget extends StatelessWidget { +typedef SinusoidChangedCallback = + void Function(double v, double f, double theta); + +void defaultChangeCallback(double v, double f, double theta) {} + +class SinusoidInputWidget extends StatefulWidget { + final SinusoidChangedCallback onChanged; static const _scaler = TextScaler.linear(1); static const _spacing = 5.0; - const SinusoidWidget({ + const SinusoidInputWidget({ super.key, required this.data, - required this.vController, - required this.fController, - required this.thetaController, + required this.initialValue, + this.onChanged = defaultChangeCallback, this.textScaler = _scaler, this.subscript = '\u2098', this.signalName = 'modulating', @@ -22,15 +27,11 @@ class SinusoidWidget extends StatelessWidget { this.maxTheta = 360, }); - final TextEditingController vController; - final TextEditingController fController; - final TextEditingController thetaController; + final Sinusoid initialValue; final TextScaler textScaler; final String subscript; final String signalName; - final AmData data; - final double minV; final double maxV; final double minF; @@ -38,29 +39,69 @@ class SinusoidWidget extends StatelessWidget { final double minTheta; final double maxTheta; + @override + State createState() => _SinusoidInputWidgetState(); +} + +class _SinusoidInputWidgetState extends State { + final TextEditingController _vController = TextEditingController(); + final TextEditingController _fController = TextEditingController(); + final TextEditingController _thetaController = TextEditingController(); + @override + void dispose() { + _vController.dispose(); + _fController.dispose(); + _thetaController.dispose(); + + super.dispose(); + } + + @override + void initState() { + _vController.text = widget.initialValue.amplitude.toString(); + _fController.text = widget.initialValue.frequency.toString(); + _thetaController.text = widget.initialValue.phase.toString(); + _vController.addListener(_onChanged); + _fController.addListener(_onChanged); + _thetaController.addListener(_onChanged); + super.initState(); + } + + void _onChanged() { + try { + double v = double.parse(_vController.text); + double f = double.parse(_fController.text); + double theta = double.parse(_thetaController.text); + widget.onChanged(v, f, theta); + } catch (e) { + return; + } + } + @override Widget build(BuildContext context) { return Column( - spacing: _spacing * 2, + spacing: SinusoidInputWidget._spacing * 2, children: [ Row( - spacing: _spacing, + spacing: SinusoidInputWidget._spacing, children: [ Flexible(fit: FlexFit.tight, child: Text('')), Flexible( fit: FlexFit.loose, child: TextField( - controller: vController, + controller: _vController, inputFormatters: [ - RangeTextInputFormatter(min: minV, max: maxV), + RangeTextInputFormatter(min: widget.minV, max: widget.maxV), ], decoration: InputDecoration( floatingLabelAlignment: FloatingLabelAlignment.start, floatingLabelStyle: TextStyle(fontStyle: FontStyle.italic), floatingLabelBehavior: FloatingLabelBehavior.always, - helperText: 'The $signalName signal amplitude in volts.', + helperText: + 'The ${widget.signalName} signal amplitude in volts.', suffix: Text('V'), - labelText: 'V$subscript ', + labelText: 'V${widget.subscript} ', border: OutlineInputBorder(gapPadding: 0), ), textAlign: TextAlign.right, @@ -69,17 +110,18 @@ class SinusoidWidget extends StatelessWidget { Flexible( fit: FlexFit.loose, child: TextField( - controller: fController, + controller: _fController, inputFormatters: [ - RangeTextInputFormatter(min: minF, max: maxF), + RangeTextInputFormatter(min: widget.minF, max: widget.maxF), ], decoration: InputDecoration( floatingLabelAlignment: FloatingLabelAlignment.start, floatingLabelStyle: TextStyle(fontStyle: FontStyle.italic), floatingLabelBehavior: FloatingLabelBehavior.always, - helperText: 'The $signalName signal frequency in Hz.', + helperText: + 'The ${widget.signalName} signal frequency in Hz.', suffix: Text('Hz'), - labelText: 'f$subscript ', + labelText: 'f${widget.subscript} ', border: OutlineInputBorder(gapPadding: 0), ), textAlign: TextAlign.right, @@ -88,17 +130,20 @@ class SinusoidWidget extends StatelessWidget { Flexible( fit: FlexFit.loose, child: TextField( - controller: thetaController, + controller: _thetaController, inputFormatters: [ - RangeTextInputFormatter(min: minTheta, max: maxTheta), + RangeTextInputFormatter( + min: widget.minTheta, + max: widget.maxTheta, + ), ], decoration: InputDecoration( floatingLabelAlignment: FloatingLabelAlignment.start, floatingLabelStyle: TextStyle(fontStyle: FontStyle.italic), floatingLabelBehavior: FloatingLabelBehavior.always, - helperText: 'The $signalName phase in degrees.', + helperText: 'The ${widget.signalName} phase in degrees.', suffix: Text('\u00b0'), - labelText: '\u03b8$subscript ', + labelText: '\u03b8${widget.subscript} ', border: OutlineInputBorder(gapPadding: 0), ), textAlign: TextAlign.right, diff --git a/test/utils/range_text_input_formatter_test.dart b/test/utils/range_text_input_formatter_test.dart new file mode 100644 index 0000000..b898a4d --- /dev/null +++ b/test/utils/range_text_input_formatter_test.dart @@ -0,0 +1,32 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:amgraph/utils/range_text_input_formatter.dart'; + +void main() { + var valueThree = TextEditingValue(text: '3'); + var valueFive = TextEditingValue(text: '5'); + var valueSeven = TextEditingValue(text: '7'); + var valueEmpty = TextEditingValue(text: ''); + var valueText = TextEditingValue(text: 'a'); + + test('Above Range', () { + var uut = RangeTextInputFormatter(min: -5, max: 5); + expect(uut.formatEditUpdate(valueThree, valueSeven), valueThree); + }); + test('Below Range', () { + var uut = RangeTextInputFormatter(min: 4, max: 10); + expect(uut.formatEditUpdate(valueSeven, valueThree), valueSeven); + }); + test('In Range', () { + var uut = RangeTextInputFormatter(min: 4, max: 10); + expect(uut.formatEditUpdate(valueFive, valueSeven), valueSeven); + }); + test('Empty', () { + var uut = RangeTextInputFormatter(min: 4, max: 10); + expect(uut.formatEditUpdate(valueFive, valueEmpty), valueFive); + }); + test('Text', () { + var uut = RangeTextInputFormatter(min: 4, max: 10); + expect(uut.formatEditUpdate(valueFive, valueText), valueFive); + }); +} diff --git a/test/widget_test.dart b/test/widget_test.dart index 0305b2c..9c01903 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,30 +1,9 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:amgraph/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. await tester.pumpWidget(const AmApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); }); } diff --git a/test/widgets/sinusoid_input_widget_test.dart b/test/widgets/sinusoid_input_widget_test.dart new file mode 100644 index 0000000..75dc16b --- /dev/null +++ b/test/widgets/sinusoid_input_widget_test.dart @@ -0,0 +1,24 @@ +import 'package:amgraph/am_data.dart'; +import 'package:amgraph/widgets/sinusoid_input_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + AmData data = AmData(); + testWidgets('Initial Value', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SinusoidInputWidget( + data: data, + initialValue: Sinusoid(3, 4000, -45), + ), + ), + ), + ); + + expect(find.widgetWithText(TextField, '4000.0'), findsOneWidget); + expect(find.widgetWithText(TextField, '3.0'), findsOneWidget); + expect(find.widgetWithText(TextField, '-45.0'), findsOneWidget); + }); +}