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.
samples/simplistic_calculator/lib/main.dart

491 lines
13 KiB

// Copyright 2022 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:io' show Platform;
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter_layout_grid/flutter_layout_grid.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:math_expressions/math_expressions.dart';
import 'package:window_size/window_size.dart';
void main() {
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
WidgetsFlutterBinding.ensureInitialized();
setWindowTitle('Simplistic Calculator');
setWindowMinSize(const Size(600, 500));
}
runApp(
const ProviderScope(
child: CalculatorApp(),
),
);
}
@immutable
class CalculatorState {
const CalculatorState({
required this.buffer,
required this.calcHistory,
required this.mode,
required this.error,
});
final String buffer;
final List<String> calcHistory;
final CalculatorEngineMode mode;
final String error;
CalculatorState copyWith({
String? buffer,
List<String>? calcHistory,
CalculatorEngineMode? mode,
String? error,
}) =>
CalculatorState(
buffer: buffer ?? this.buffer,
calcHistory: calcHistory ?? this.calcHistory,
mode: mode ?? this.mode,
error: error ?? this.error,
);
}
enum CalculatorEngineMode { input, result }
class CalculatorEngine extends StateNotifier<CalculatorState> {
CalculatorEngine()
: super(
const CalculatorState(
buffer: '0',
calcHistory: [],
mode: CalculatorEngineMode.result,
error: '',
),
);
void addToBuffer(String str, {bool continueWithResult = false}) {
if (state.mode == CalculatorEngineMode.result) {
state = state.copyWith(
buffer: (continueWithResult ? state.buffer : '') + str,
mode: CalculatorEngineMode.input,
error: '',
);
} else {
state = state.copyWith(
buffer: state.buffer + str,
error: '',
);
}
}
void backspace() {
final charList = Characters(state.buffer).toList();
if (charList.isNotEmpty) {
charList.length = charList.length - 1;
}
state = state.copyWith(buffer: charList.join());
}
void clear() {
state = state.copyWith(buffer: '');
}
void evaluate() {
try {
final parser = Parser();
final cm = ContextModel();
final exp = parser.parse(state.buffer);
final result = exp.evaluate(EvaluationType.REAL, cm) as double;
switch (result) {
case double(isInfinite: true):
state = state.copyWith(
error: 'Result is Infinite',
buffer: '',
mode: CalculatorEngineMode.result,
);
case double(isNaN: true):
state = state.copyWith(
error: 'Result is Not a Number',
buffer: '',
mode: CalculatorEngineMode.result,
);
default:
final resultStr = result.ceil() == result
? result.toInt().toString()
: result.toString();
state = state.copyWith(
buffer: resultStr,
mode: CalculatorEngineMode.result,
calcHistory: [
'${state.buffer} = $resultStr',
...state.calcHistory,
]);
}
} catch (err) {
state = state.copyWith(
error: err.toString(),
buffer: '',
mode: CalculatorEngineMode.result,
);
}
}
}
final calculatorStateProvider =
StateNotifierProvider<CalculatorEngine, CalculatorState>(
(_) => CalculatorEngine());
class ButtonDefinition {
const ButtonDefinition({
required this.areaName,
required this.label,
required this.op,
this.type = CalcButtonType.outlined,
});
final String areaName;
final String label;
final CalculatorEngineCallback op;
final CalcButtonType type;
}
final buttonDefinitions = <ButtonDefinition>[
ButtonDefinition(
areaName: 'clear',
op: (engine) => engine.clear(),
label: 'AC',
),
ButtonDefinition(
areaName: 'bkspc',
op: (engine) => engine.backspace(),
label: '',
),
ButtonDefinition(
areaName: 'lparen',
op: (engine) => engine.addToBuffer('('),
label: '(',
),
ButtonDefinition(
areaName: 'rparen',
op: (engine) => engine.addToBuffer(')'),
label: ')',
),
ButtonDefinition(
areaName: 'sqrt',
op: (engine) => engine.addToBuffer('sqrt('),
label: '',
),
ButtonDefinition(
areaName: 'pow',
op: (engine) => engine.addToBuffer('^'),
label: '^',
),
ButtonDefinition(
areaName: 'abs',
op: (engine) => engine.addToBuffer('abs('),
label: 'Abs',
),
ButtonDefinition(
areaName: 'sgn',
op: (engine) => engine.addToBuffer('sgn('),
label: 'Sgn',
),
ButtonDefinition(
areaName: 'ceil',
op: (engine) => engine.addToBuffer('ceil('),
label: 'Ceil',
),
ButtonDefinition(
areaName: 'floor',
op: (engine) => engine.addToBuffer('floor('),
label: 'Floor',
),
ButtonDefinition(
areaName: 'e',
op: (engine) => engine.addToBuffer('e('),
label: 'e',
),
ButtonDefinition(
areaName: 'ln',
op: (engine) => engine.addToBuffer('ln('),
label: 'ln',
),
ButtonDefinition(
areaName: 'sin',
op: (engine) => engine.addToBuffer('sin('),
label: 'Sin',
),
ButtonDefinition(
areaName: 'cos',
op: (engine) => engine.addToBuffer('cos('),
label: 'Cos',
),
ButtonDefinition(
areaName: 'tan',
op: (engine) => engine.addToBuffer('tan('),
label: 'Tan',
),
ButtonDefinition(
areaName: 'fact',
op: (engine) => engine.addToBuffer('!'),
label: '!',
),
ButtonDefinition(
areaName: 'arcsin',
op: (engine) => engine.addToBuffer('arcsin('),
label: 'Arc Sin',
),
ButtonDefinition(
areaName: 'arccos',
op: (engine) => engine.addToBuffer('arccos('),
label: 'Arc Cos',
),
ButtonDefinition(
areaName: 'arctan',
op: (engine) => engine.addToBuffer('arctan('),
label: 'Arc Tan',
),
ButtonDefinition(
areaName: 'mod',
op: (engine) => engine.addToBuffer('%'),
label: 'Mod',
),
ButtonDefinition(
areaName: 'seven',
op: (engine) => engine.addToBuffer('7'),
label: '7',
),
ButtonDefinition(
areaName: 'eight',
op: (engine) => engine.addToBuffer('8'),
label: '8',
),
ButtonDefinition(
areaName: 'nine',
op: (engine) => engine.addToBuffer('9'),
label: '9',
),
ButtonDefinition(
areaName: 'four',
op: (engine) => engine.addToBuffer('4'),
label: '4',
),
ButtonDefinition(
areaName: 'five',
op: (engine) => engine.addToBuffer('5'),
label: '5',
),
ButtonDefinition(
areaName: 'six',
op: (engine) => engine.addToBuffer('6'),
label: '6',
),
ButtonDefinition(
areaName: 'one',
op: (engine) => engine.addToBuffer('1'),
label: '1',
),
ButtonDefinition(
areaName: 'two',
op: (engine) => engine.addToBuffer('2'),
label: '2',
),
ButtonDefinition(
areaName: 'three',
op: (engine) => engine.addToBuffer('3'),
label: '3',
),
ButtonDefinition(
areaName: 'zero',
op: (engine) => engine.addToBuffer('0'),
label: '0',
),
ButtonDefinition(
areaName: 'point',
op: (engine) => engine.addToBuffer('.'),
label: '.',
),
ButtonDefinition(
areaName: 'equals',
op: (engine) => engine.evaluate(),
label: '=',
type: CalcButtonType.elevated,
),
ButtonDefinition(
areaName: 'plus',
op: (engine) => engine.addToBuffer('+', continueWithResult: true),
label: '+',
),
ButtonDefinition(
areaName: 'minus',
op: (engine) => engine.addToBuffer('-', continueWithResult: true),
label: '-',
),
ButtonDefinition(
areaName: 'multiply',
op: (engine) => engine.addToBuffer('*', continueWithResult: true),
label: '*',
),
ButtonDefinition(
areaName: 'divide',
op: (engine) => engine.addToBuffer('/', continueWithResult: true),
label: '/',
),
];
class CalculatorApp extends ConsumerWidget {
const CalculatorApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(calculatorStateProvider);
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData.light(),
home: Scaffold(
body: Container(
color: Colors.white,
child: SafeArea(
child: LayoutGrid(
areas: '''
display display display display history
clear bkspc lparen rparen history
sqrt pow abs sgn history
ceil floor e ln history
sin cos tan fact history
arcsin arccos arctan mod history
seven eight nine divide history
four five six multiply history
one two three minus history
zero point equals plus history
''',
columnSizes: [1.fr, 1.fr, 1.fr, 1.fr, 2.fr],
rowSizes: [
2.fr,
1.fr,
1.fr,
1.fr,
1.fr,
1.fr,
2.fr,
2.fr,
2.fr,
2.fr
],
children: [
NamedAreaGridPlacement(
areaName: 'display',
child: SizedBox.expand(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 8),
child: state.error.isEmpty
? AutoSizeText(
state.buffer,
textAlign: TextAlign.end,
style: const TextStyle(
fontSize: 80,
color: Colors.black,
),
maxLines: 2,
)
: AutoSizeText(
state.error,
textAlign: TextAlign.start,
style: const TextStyle(
fontSize: 80,
color: Colors.red,
),
maxLines: 2,
),
),
),
),
...buttonDefinitions.map(
(definition) => NamedAreaGridPlacement(
areaName: definition.areaName,
child: CalcButton(
label: definition.label,
op: definition.op,
type: definition.type,
),
),
),
NamedAreaGridPlacement(
areaName: 'history',
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: ListView(
children: [
const ListTile(
title: Text(
'History',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
...state.calcHistory.map(
(result) => ListTile(
title: Text(result),
),
)
],
),
),
),
],
),
),
),
),
);
}
}
typedef CalculatorEngineCallback = void Function(CalculatorEngine engine);
enum CalcButtonType { outlined, elevated }
class CalcButton extends ConsumerWidget {
const CalcButton({
super.key,
required this.op,
required this.label,
required this.type,
});
final CalculatorEngineCallback op;
final String label;
final CalcButtonType type;
@override
Widget build(BuildContext context, WidgetRef ref) {
final buttonConstructor = switch (type) {
CalcButtonType.elevated => ElevatedButton.new,
_ => OutlinedButton.new,
};
return SizedBox.expand(
child: Padding(
padding: const EdgeInsets.all(4),
child: buttonConstructor(
autofocus: false,
clipBehavior: Clip.none,
onPressed: () => op(ref.read(calculatorStateProvider.notifier)),
child: AutoSizeText(
label,
style: const TextStyle(fontSize: 40, color: Colors.black54),
),
),
),
);
}
}