Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
c9b5173abd | ||
|
f3cee84c51 | ||
|
335079ac14 | ||
|
c713e0dbd1 | ||
|
191b2da96c | ||
|
033d2d973a | ||
|
9a12104044 | ||
|
cd1eb22a7d | ||
|
b8c2f24f6e | ||
|
8346412f7a | ||
|
d935cf6101 | ||
|
78498fbfcd | ||
|
0e72baa72c |
3398
Cargo.lock
generated
3398
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
15
Cargo.toml
15
Cargo.toml
@ -11,13 +11,24 @@ repository = "https://"
|
|||||||
keywords = ["tui", "cli", "rpn"]
|
keywords = ["tui", "cli", "rpn"]
|
||||||
categories = ["command-line-utilities"]
|
categories = ["command-line-utilities"]
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
".",
|
||||||
|
"./rpn_rs_tui/",
|
||||||
|
"./rpn_rs_gui/",
|
||||||
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
crossterm = "0.18"
|
|
||||||
tui = { version = "0.14", default-features = false, features = ['crossterm'] }
|
|
||||||
serde = {version = "1.0", features = ["derive"]}
|
serde = {version = "1.0", features = ["derive"]}
|
||||||
# confy = "0.4.0"
|
# confy = "0.4.0"
|
||||||
toml = "0.4.2"
|
toml = "0.4.2"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
|
rust_decimal = { version = "1.29.1", features=["maths"] }
|
||||||
|
rust_decimal_macros = "1.29.1"
|
||||||
|
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||||
|
tracing-appender = "0.2.2"
|
||||||
|
tracing = "0.1.37"
|
||||||
|
egui_extras = "0.22.0"
|
||||||
|
|
||||||
[dependencies.confy]
|
[dependencies.confy]
|
||||||
# TODO: Update this to v0.5.0 when it finally comes out -- for now, use latest git master
|
# TODO: Update this to v0.5.0 when it finally comes out -- for now, use latest git master
|
||||||
|
@ -133,3 +133,7 @@ Will I implement these features? I don't know. Lots of these could be done by se
|
|||||||
* Bases: Not yet
|
* Bases: Not yet
|
||||||
* Different math operators like `!` or `sum`: If someone asks me to, I guess
|
* Different math operators like `!` or `sum`: If someone asks me to, I guess
|
||||||
* Conditionals: If someone asks me to, I guess
|
* Conditionals: If someone asks me to, I guess
|
||||||
|
|
||||||
|
=== Credits
|
||||||
|
|
||||||
|
* LCD Solid Font licensed as public domain from: https://www.fontspace.com/lcd-solid-font-f11346
|
||||||
|
6
rpn_rs_gui/.cargo/config.toml
Normal file
6
rpn_rs_gui/.cargo/config.toml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# clipboard api is still unstable, so web-sys requires the below flag to be passed for copy (ctrl + c) to work
|
||||||
|
# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html
|
||||||
|
# check status at https://developer.mozilla.org/en-US/docs/Web/API/Clipboard#browser_compatibility
|
||||||
|
# we don't use `[build]` because of rust analyzer's build cache invalidation https://github.com/emilk/eframe_template/issues/93
|
||||||
|
[target.wasm32-unknown-unknown]
|
||||||
|
rustflags = ["--cfg=web_sys_unstable_apis"]
|
45
rpn_rs_gui/.github/workflows/pages.yml
vendored
Normal file
45
rpn_rs_gui/.github/workflows/pages.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
name: Github Pages
|
||||||
|
|
||||||
|
# By default, runs if you push to master. keeps your deployed app in sync with master branch.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
# to only run when you do a new github release, comment out above part and uncomment the below trigger.
|
||||||
|
# on:
|
||||||
|
# release:
|
||||||
|
# types:
|
||||||
|
# - published
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write # for committing to gh-pages branch.
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-github-pages:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2 # repo checkout
|
||||||
|
- uses: actions-rs/toolchain@v1 # get rust toolchain for wasm
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: stable
|
||||||
|
target: wasm32-unknown-unknown
|
||||||
|
override: true
|
||||||
|
- name: Rust Cache # cache the rust build artefacts
|
||||||
|
uses: Swatinem/rust-cache@v1
|
||||||
|
- name: Download and install Trunk binary
|
||||||
|
run: wget -qO- https://github.com/thedodd/trunk/releases/latest/download/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf-
|
||||||
|
- name: Build # build
|
||||||
|
# "${GITHUB_REPOSITORY#*/}" evaluates into the name of the repository
|
||||||
|
# using --public-url something will allow trunk to modify all the href paths like from favicon.ico to repo_name/favicon.ico .
|
||||||
|
# this is necessary for github pages where the site is deployed to username.github.io/repo_name and all files must be requested
|
||||||
|
# relatively as eframe_template/favicon.ico. if we skip public-url option, the href paths will instead request username.github.io/favicon.ico which
|
||||||
|
# will obviously return error 404 not found.
|
||||||
|
run: ./trunk build --release --public-url "${GITHUB_REPOSITORY#*/}"
|
||||||
|
- name: Deploy
|
||||||
|
uses: JamesIves/github-pages-deploy-action@v4
|
||||||
|
with:
|
||||||
|
folder: dist
|
||||||
|
# this option will not maintain any history of your previous pages deployment
|
||||||
|
# set to false if you want all page build to be committed to your gh-pages branch history
|
||||||
|
single-commit: true
|
105
rpn_rs_gui/.github/workflows/rust.yml
vendored
Normal file
105
rpn_rs_gui/.github/workflows/rust.yml
vendored
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
env:
|
||||||
|
# This is required to enable the web_sys clipboard API which egui_web uses
|
||||||
|
# https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html
|
||||||
|
# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html
|
||||||
|
RUSTFLAGS: --cfg=web_sys_unstable_apis
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
name: Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: check
|
||||||
|
args: --all-features
|
||||||
|
|
||||||
|
check_wasm:
|
||||||
|
name: Check wasm32
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: stable
|
||||||
|
target: wasm32-unknown-unknown
|
||||||
|
override: true
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: check
|
||||||
|
args: --all-features --lib --target wasm32-unknown-unknown
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Test Suite
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
|
- run: sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: test
|
||||||
|
args: --lib
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
name: Rustfmt
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
|
components: rustfmt
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: fmt
|
||||||
|
args: --all -- --check
|
||||||
|
|
||||||
|
clippy:
|
||||||
|
name: Clippy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
|
components: clippy
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: clippy
|
||||||
|
args: -- -D warnings
|
||||||
|
|
||||||
|
trunk:
|
||||||
|
name: trunk
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: 1.65.0
|
||||||
|
target: wasm32-unknown-unknown
|
||||||
|
override: true
|
||||||
|
- name: Download and install Trunk binary
|
||||||
|
run: wget -qO- https://github.com/thedodd/trunk/releases/latest/download/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf-
|
||||||
|
- name: Build
|
||||||
|
run: ./trunk build
|
2
rpn_rs_gui/.gitignore
vendored
Normal file
2
rpn_rs_gui/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
/dist
|
2626
rpn_rs_gui/Cargo.lock
generated
Normal file
2626
rpn_rs_gui/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
48
rpn_rs_gui/Cargo.toml
Normal file
48
rpn_rs_gui/Cargo.toml
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
[package]
|
||||||
|
name = "rpn_rs_gui"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Austen Adler <agadler@austenadler.com>"]
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.65"
|
||||||
|
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rpn_rs = {path=".."}
|
||||||
|
egui = "0.22.0"
|
||||||
|
eframe = { version = "0.22.0", default-features = false, features = ["accesskit", "default_fonts", "glow", "persistence"] }
|
||||||
|
|
||||||
|
# You only need serde if you want app persistence:
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
tracing = "0.1.37"
|
||||||
|
egui_extras = "0.22.0"
|
||||||
|
|
||||||
|
# native:
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
tracing-subscriber = "0.3"
|
||||||
|
|
||||||
|
# web:
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
console_error_panic_hook = "0.1.6"
|
||||||
|
tracing-wasm = "0.2"
|
||||||
|
wasm-bindgen = { version = "0.2.87" }
|
||||||
|
wasm-bindgen-futures = "0.4"
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: This block was commented
|
||||||
|
# [profile.release]
|
||||||
|
# opt-level = 2 # fast and small wasm
|
||||||
|
|
||||||
|
# # Optimize all dependencies even in debug builds:
|
||||||
|
# [profile.dev.package."*"]
|
||||||
|
# opt-level = 2
|
||||||
|
|
||||||
|
# TODO: This block was commented
|
||||||
|
# [patch.crates-io]
|
||||||
|
|
||||||
|
# If you want to use the bleeding edge version of egui and eframe:
|
||||||
|
# egui = { git = "https://github.com/emilk/egui", branch = "master" }
|
||||||
|
# eframe = { git = "https://github.com/emilk/egui", branch = "master" }
|
||||||
|
|
||||||
|
# If you fork https://github.com/emilk/egui you can test with:
|
||||||
|
# egui = { path = "../egui/crates/egui" }
|
||||||
|
# eframe = { path = "../egui/crates/eframe" }
|
BIN
rpn_rs_gui/LcdSolid-VPzB.ttf
Normal file
BIN
rpn_rs_gui/LcdSolid-VPzB.ttf
Normal file
Binary file not shown.
75
rpn_rs_gui/README.md
Normal file
75
rpn_rs_gui/README.md
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# eframe template
|
||||||
|
|
||||||
|
[![dependency status](https://deps.rs/repo/github/emilk/eframe_template/status.svg)](https://deps.rs/repo/github/emilk/eframe_template)
|
||||||
|
[![Build Status](https://github.com/emilk/eframe_template/workflows/CI/badge.svg)](https://github.com/emilk/eframe_template/actions?workflow=CI)
|
||||||
|
|
||||||
|
This is a template repo for [eframe](https://github.com/emilk/egui/tree/master/crates/eframe), a framework for writing apps using [egui](https://github.com/emilk/egui/).
|
||||||
|
|
||||||
|
The goal is for this to be the simplest way to get started writing a GUI app in Rust.
|
||||||
|
|
||||||
|
You can compile your app natively or for the web, and share it using Github Pages.
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
Start by clicking "Use this template" at https://github.com/emilk/eframe_template/ or follow [these instructions](https://docs.github.com/en/free-pro-team@latest/github/creating-cloning-and-archiving-repositories/creating-a-repository-from-a-template).
|
||||||
|
|
||||||
|
Change the name of the crate: Chose a good name for your project, and change the name to it in:
|
||||||
|
* `Cargo.toml`
|
||||||
|
* Change the `package.name` from `eframe_template` to `your_crate`.
|
||||||
|
* Change the `package.authors`
|
||||||
|
* `main.rs`
|
||||||
|
* Change `eframe_template::TemplateApp` to `your_crate::TemplateApp`
|
||||||
|
* `index.html`
|
||||||
|
* Change the `<title>eframe template</title>` to `<title>your_crate</title>`. optional.
|
||||||
|
* `assets/sw.js`
|
||||||
|
* Change the `'./eframe_template.js'` to `./your_crate.js` (in `filesToCache` array)
|
||||||
|
* Change the `'./eframe_template_bg.wasm'` to `./your_crate_bg.wasm` (in `filesToCache` array)
|
||||||
|
|
||||||
|
### Learning about egui
|
||||||
|
|
||||||
|
`src/app.rs` contains a simple example app. This is just to give some inspiration - most of it can be removed if you like.
|
||||||
|
|
||||||
|
The official egui docs are at <https://docs.rs/egui>. If you prefer watching a video introduction, check out <https://www.youtube.com/watch?v=NtUkr_z7l84>. For inspiration, check out the [the egui web demo](https://emilk.github.io/egui/index.html) and follow the links in it to its source code.
|
||||||
|
|
||||||
|
### Testing locally
|
||||||
|
|
||||||
|
Make sure you are using the latest version of stable rust by running `rustup update`.
|
||||||
|
|
||||||
|
`cargo run --release`
|
||||||
|
|
||||||
|
On Linux you need to first run:
|
||||||
|
|
||||||
|
`sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev`
|
||||||
|
|
||||||
|
On Fedora Rawhide you need to run:
|
||||||
|
|
||||||
|
`dnf install clang clang-devel clang-tools-extra libxkbcommon-devel pkg-config openssl-devel libxcb-devel fontconfig-devel`
|
||||||
|
|
||||||
|
### Web Locally
|
||||||
|
|
||||||
|
You can compile your app to [WASM](https://en.wikipedia.org/wiki/WebAssembly) and publish it as a web page.
|
||||||
|
|
||||||
|
We use [Trunk](https://trunkrs.dev/) to build for web target.
|
||||||
|
1. Install Trunk with `cargo install --locked trunk`.
|
||||||
|
2. Run `trunk serve` to build and serve on `http://127.0.0.1:8080`. Trunk will rebuild automatically if you edit the project.
|
||||||
|
3. Open `http://127.0.0.1:8080/index.html#dev` in a browser. See the warning below.
|
||||||
|
|
||||||
|
> `assets/sw.js` script will try to cache our app, and loads the cached version when it cannot connect to server allowing your app to work offline (like PWA).
|
||||||
|
> appending `#dev` to `index.html` will skip this caching, allowing us to load the latest builds during development.
|
||||||
|
|
||||||
|
### Web Deploy
|
||||||
|
1. Just run `trunk build --release`.
|
||||||
|
2. It will generate a `dist` directory as a "static html" website
|
||||||
|
3. Upload the `dist` directory to any of the numerous free hosting websites including [GitHub Pages](https://docs.github.com/en/free-pro-team@latest/github/working-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site).
|
||||||
|
4. we already provide a workflow that auto-deploys our app to GitHub pages if you enable it.
|
||||||
|
> To enable Github Pages, you need to go to Repository -> Settings -> Pages -> Source -> set to `gh-pages` branch and `/` (root).
|
||||||
|
>
|
||||||
|
> If `gh-pages` is not available in `Source`, just create and push a branch called `gh-pages` and it should be available.
|
||||||
|
|
||||||
|
You can test the template app at <https://emilk.github.io/eframe_template/>.
|
||||||
|
|
||||||
|
## Updating egui
|
||||||
|
|
||||||
|
As of 2022, egui is in active development with frequent releases with breaking changes. [eframe_template](https://github.com/emilk/eframe_template/) will be updated in lock-step to always use the latest version of egui.
|
||||||
|
|
||||||
|
When updating `egui` and `eframe` it is recommended you do so one version at the time, and read about the changes in [the egui changelog](https://github.com/emilk/egui/blob/master/CHANGELOG.md) and [eframe changelog](https://github.com/emilk/egui/blob/master/crates/eframe/CHANGELOG.md).
|
2
rpn_rs_gui/Trunk.toml
Normal file
2
rpn_rs_gui/Trunk.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[build]
|
||||||
|
filehash = false
|
BIN
rpn_rs_gui/assets/favicon.ico
Executable file
BIN
rpn_rs_gui/assets/favicon.ico
Executable file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
rpn_rs_gui/assets/icon-1024.png
Normal file
BIN
rpn_rs_gui/assets/icon-1024.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 314 KiB |
BIN
rpn_rs_gui/assets/icon-256.png
Normal file
BIN
rpn_rs_gui/assets/icon-256.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
BIN
rpn_rs_gui/assets/icon_ios_touch_192.png
Normal file
BIN
rpn_rs_gui/assets/icon_ios_touch_192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
28
rpn_rs_gui/assets/manifest.json
Normal file
28
rpn_rs_gui/assets/manifest.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "egui Template PWA",
|
||||||
|
"short_name": "egui-template-pwa",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "./icon-256.png",
|
||||||
|
"sizes": "256x256",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "./maskable_icon_x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "./icon-1024.png",
|
||||||
|
"sizes": "1024x1024",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lang": "en-US",
|
||||||
|
"id": "/index.html",
|
||||||
|
"start_url": "./index.html",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "white",
|
||||||
|
"theme_color": "white"
|
||||||
|
}
|
BIN
rpn_rs_gui/assets/maskable_icon_x512.png
Normal file
BIN
rpn_rs_gui/assets/maskable_icon_x512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 128 KiB |
30
rpn_rs_gui/assets/sw.js
Normal file
30
rpn_rs_gui/assets/sw.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
var cacheName = 'egui-template-pwa';
|
||||||
|
var filesToCache = [
|
||||||
|
'./',
|
||||||
|
'./index.html',
|
||||||
|
'./eframe_template.js',
|
||||||
|
'./eframe_template_bg.wasm',
|
||||||
|
];
|
||||||
|
|
||||||
|
// self.addEventListener('keydown', function (e) {
|
||||||
|
// console.log("Got keydown", e);
|
||||||
|
// e.preventDefault();
|
||||||
|
// })
|
||||||
|
|
||||||
|
/* Start the service worker and cache all of the app's content */
|
||||||
|
self.addEventListener('install', function (e) {
|
||||||
|
e.waitUntil(
|
||||||
|
caches.open(cacheName).then(function (cache) {
|
||||||
|
return cache.addAll(filesToCache);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Serve cached content when offline */
|
||||||
|
self.addEventListener('fetch', function (e) {
|
||||||
|
e.respondWith(
|
||||||
|
caches.match(e.request).then(function (response) {
|
||||||
|
return response || fetch(e.request);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
11
rpn_rs_gui/check.sh
Executable file
11
rpn_rs_gui/check.sh
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# This scripts runs various CI-like checks in a convenient way.
|
||||||
|
set -eux
|
||||||
|
|
||||||
|
cargo check --workspace --all-targets
|
||||||
|
cargo check --workspace --all-features --lib --target wasm32-unknown-unknown
|
||||||
|
cargo fmt --all -- --check
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings -W clippy::all
|
||||||
|
cargo test --workspace --all-targets --all-features
|
||||||
|
cargo test --workspace --doc
|
||||||
|
trunk build
|
140
rpn_rs_gui/index.html
Normal file
140
rpn_rs_gui/index.html
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
|
||||||
|
<!-- Disable zooming: -->
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<!-- change this to your project name -->
|
||||||
|
<title>RPN RS2</title>
|
||||||
|
|
||||||
|
<!-- config for our rust wasm binary. go to https://trunkrs.dev/assets/#rust for more customization -->
|
||||||
|
<link data-trunk rel="rust" data-wasm-opt="2" />
|
||||||
|
<!-- this is the base url relative to which other urls will be constructed. trunk will insert this from the public-url option -->
|
||||||
|
<base data-trunk-public-url />
|
||||||
|
|
||||||
|
<link data-trunk rel="icon" href="assets/favicon.ico">
|
||||||
|
|
||||||
|
|
||||||
|
<link data-trunk rel="copy-file" href="assets/sw.js" />
|
||||||
|
<link data-trunk rel="copy-file" href="assets/manifest.json" />
|
||||||
|
<link data-trunk rel="copy-file" href="assets/icon-1024.png" />
|
||||||
|
<link data-trunk rel="copy-file" href="assets/icon-256.png" />
|
||||||
|
<link data-trunk rel="copy-file" href="assets/icon_ios_touch_192.png" />
|
||||||
|
<link data-trunk rel="copy-file" href="assets/maskable_icon_x512.png" />
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="manifest" href="manifest.json">
|
||||||
|
<link rel="apple-touch-icon" href="icon_ios_touch_192.png">
|
||||||
|
<meta name="theme-color" media="(prefers-color-scheme: light)" content="white">
|
||||||
|
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#404040">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
/* Remove touch delay: */
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
/* Light mode background color for what is not covered by the egui canvas,
|
||||||
|
or where the egui canvas is translucent. */
|
||||||
|
background: #909090;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
/* Dark mode background color for what is not covered by the egui canvas,
|
||||||
|
or where the egui canvas is translucent. */
|
||||||
|
background: #404040;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Allow canvas to fill entire web page: */
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Position canvas in center-top: */
|
||||||
|
canvas {
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.centered {
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: #f0f0f0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-family: Ubuntu-Light, Helvetica, sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------- */
|
||||||
|
/* Loading animation from https://loading.io/css/ */
|
||||||
|
.lds-dual-ring {
|
||||||
|
display: inline-block;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lds-dual-ring:after {
|
||||||
|
content: " ";
|
||||||
|
display: block;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin: 0px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid #fff;
|
||||||
|
border-color: #fff transparent #fff transparent;
|
||||||
|
animation: lds-dual-ring 1.2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes lds-dual-ring {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!-- The WASM code will resize the canvas dynamically -->
|
||||||
|
<!-- the id is hardcoded in main.rs . so, make sure both match. -->
|
||||||
|
<canvas id="the_canvas_id"></canvas>
|
||||||
|
|
||||||
|
<!--Register Service Worker. this will cache the wasm / js scripts for offline use (for PWA functionality). -->
|
||||||
|
<!-- Force refresh (Ctrl + F5) to load the latest files instead of cached files -->
|
||||||
|
<script>
|
||||||
|
// We disable caching during development so that we always view the latest version.
|
||||||
|
if ('serviceWorker' in navigator && window.location.hash !== "#dev") {
|
||||||
|
window.addEventListener('load', function () {
|
||||||
|
navigator.serviceWorker.register('sw.js');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<!-- Powered by egui: https://github.com/emilk/egui/ -->
|
539
rpn_rs_gui/src/app.rs
Normal file
539
rpn_rs_gui/src/app.rs
Normal file
@ -0,0 +1,539 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use egui::{
|
||||||
|
Align, Button, Color32, Direction, FontData, FontDefinitions, FontFamily, FontId, Frame, Grid,
|
||||||
|
Id, Key, Label, Layout, Margin, PointerButton, Pos2, Rect, RichText, Rounding, ScrollArea,
|
||||||
|
Sense, Stroke, Style, TouchId, TouchPhase, Vec2,
|
||||||
|
};
|
||||||
|
use egui_extras::{Column, Size, StripBuilder, TableBuilder};
|
||||||
|
use rpn_rs::calc::{
|
||||||
|
entries::CalculatorEntry,
|
||||||
|
errors::CalculatorError,
|
||||||
|
types::{CalculatorDisplayMode, CalculatorState},
|
||||||
|
Calculator,
|
||||||
|
};
|
||||||
|
use tracing::{error, info};
|
||||||
|
mod buttons;
|
||||||
|
use buttons::CalculatorButton;
|
||||||
|
|
||||||
|
use self::buttons::{BUTTON_LAYOUT, BUTTON_LAYOUT_SETTINGS};
|
||||||
|
|
||||||
|
const DEFAULT_FONT_SIZE: f32 = 45.0;
|
||||||
|
const STACK_FONT_SIZE: f32 = 25.0;
|
||||||
|
const DEFAULT_FONT: FontId = FontId::monospace(DEFAULT_FONT_SIZE);
|
||||||
|
// const BUTTON_SIZE_WIDTH: Size = Size::remainder();
|
||||||
|
// const BUTTON_SIZE_HEIGHT: Size = Size::remainder();
|
||||||
|
// const BUTTON_SIZE: Vec2 = Vec2 { x: 67.0, y: 60.0 };
|
||||||
|
const BUTTON_SPACING: Vec2 = Vec2 { x: 0.0, y: 0.0 };
|
||||||
|
// const BUTTON_PADDING: Vec2 = Vec2 { x: 24.0, y: 5.0 };
|
||||||
|
const BUTTON_PADDING: Vec2 = Vec2 { x: 0.0, y: 0.0 };
|
||||||
|
|
||||||
|
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
|
||||||
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct TemplateApp {
|
||||||
|
calculator: CalculatorInner,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
latest_error: Option<CalculatorError>,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
touches_down: HashSet<ClickTapId>,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
new_touches: Vec<Pos2>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, serde::Serialize, Default)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct CalculatorInner {
|
||||||
|
calculator: Calculator,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
error_state: ErrorState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CalculatorInner {
|
||||||
|
fn calculator_input(&mut self, c: char) {
|
||||||
|
let action = if c == '$' {
|
||||||
|
self.calculator.backspace()
|
||||||
|
} else {
|
||||||
|
self.calculator.take_input(c)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = action {
|
||||||
|
self.error_state.errored(e);
|
||||||
|
} else {
|
||||||
|
self.error_state.success();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Phase of a click or tap
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
enum ClickPhase {
|
||||||
|
Start,
|
||||||
|
End,
|
||||||
|
Ignored,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ID of a click or tap
|
||||||
|
///
|
||||||
|
/// Required because taps have IDs, but clicks don't
|
||||||
|
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||||
|
enum ClickTapId {
|
||||||
|
Tap(u64),
|
||||||
|
Click(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&TouchId> for ClickTapId {
|
||||||
|
fn from(value: &TouchId) -> Self {
|
||||||
|
Self::Tap(value.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&PointerButton> for ClickTapId {
|
||||||
|
fn from(value: &PointerButton) -> Self {
|
||||||
|
Self::Click(*value as u8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&TouchPhase> for ClickPhase {
|
||||||
|
fn from(value: &TouchPhase) -> Self {
|
||||||
|
match value {
|
||||||
|
TouchPhase::Start => Self::Start,
|
||||||
|
TouchPhase::End => Self::End,
|
||||||
|
TouchPhase::Move | TouchPhase::Cancel => Self::Ignored,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TemplateApp {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
calculator: CalculatorInner::default(),
|
||||||
|
latest_error: None,
|
||||||
|
touches_down: HashSet::new(),
|
||||||
|
new_touches: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TemplateApp {
|
||||||
|
/// Called once before the first frame.
|
||||||
|
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||||
|
// This is also where you can customize the look and feel of egui using
|
||||||
|
// `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`.
|
||||||
|
|
||||||
|
// Load previous app state (if any).
|
||||||
|
// Note that you must enable the `persistence` feature for this to work.
|
||||||
|
let ret;
|
||||||
|
if let Some(storage) = cc.storage {
|
||||||
|
ret = eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default();
|
||||||
|
} else {
|
||||||
|
ret = Default::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
TemplateApp::initialize_fonts(&cc.egui_ctx);
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_stack(&mut self, ui: &mut egui::Ui) {
|
||||||
|
TableBuilder::new(ui)
|
||||||
|
.stick_to_bottom(true)
|
||||||
|
// .column(Column::auto() )
|
||||||
|
.column(Column::remainder())
|
||||||
|
// .header(20.0, |mut header| {
|
||||||
|
// header.col(|ui| {
|
||||||
|
// ui.heading("Entry");
|
||||||
|
// });
|
||||||
|
// header.col(|ui| {
|
||||||
|
// ui.heading("Value");
|
||||||
|
// });
|
||||||
|
// })
|
||||||
|
.body(|mut body| {
|
||||||
|
for (_idx, entry) in self.calculator.calculator.stack.iter().enumerate().rev() {
|
||||||
|
body.row(30.0, |mut row| {
|
||||||
|
// row.col(|ui| {
|
||||||
|
// ui.add(Label::new(
|
||||||
|
// RichText::new(format!("{idx}"))
|
||||||
|
// .background_color(Color32::RED)
|
||||||
|
// .font(DEFAULT_FONT)
|
||||||
|
// .size(STACK_FONT_SIZE)
|
||||||
|
// .color(Color32::WHITE)
|
||||||
|
// ));
|
||||||
|
// });
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.add(Label::new(
|
||||||
|
RichText::new(
|
||||||
|
entry.format_entry(&self.calculator.calculator.display_mode),
|
||||||
|
)
|
||||||
|
.background_color(Color32::DARK_GRAY)
|
||||||
|
.font(DEFAULT_FONT)
|
||||||
|
.size(STACK_FONT_SIZE)
|
||||||
|
.color(Color32::WHITE),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_error(&mut self, ui: &mut egui::Ui) {
|
||||||
|
if let Some(ref e) = self.latest_error {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(e.to_string())
|
||||||
|
.font(DEFAULT_FONT)
|
||||||
|
.size(STACK_FONT_SIZE)
|
||||||
|
.background_color(Color32::RED)
|
||||||
|
.color(Color32::WHITE),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_input(&mut self, ui: &mut egui::Ui) {
|
||||||
|
ui.painter()
|
||||||
|
.rect_filled(ui.available_rect_before_wrap(), 0.0, Color32::LIGHT_GREEN);
|
||||||
|
ui.label(
|
||||||
|
RichText::new(self.calculator.calculator.get_l())
|
||||||
|
.color(Color32::BLACK)
|
||||||
|
.font(FontId::monospace(30.0)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_buttons(&mut self, ui: &mut egui::Ui) {
|
||||||
|
let button_layout = match self.calculator.calculator.state {
|
||||||
|
CalculatorState::Normal => BUTTON_LAYOUT,
|
||||||
|
CalculatorState::WaitingForConstant => return,
|
||||||
|
CalculatorState::WaitingForMacro => return,
|
||||||
|
CalculatorState::WaitingForRegister(_) => return,
|
||||||
|
CalculatorState::WaitingForSetting => BUTTON_LAYOUT_SETTINGS,
|
||||||
|
};
|
||||||
|
|
||||||
|
StripBuilder::new(ui)
|
||||||
|
.sizes(Size::exact(60.0), button_layout.len())
|
||||||
|
// .sizes(Size::remainder(), button_layout.len())
|
||||||
|
.vertical(|mut strip| {
|
||||||
|
for row in button_layout.iter() {
|
||||||
|
strip.strip(|builder| {
|
||||||
|
builder
|
||||||
|
.sizes(Size::remainder(), row.len())
|
||||||
|
.horizontal(|mut strip| {
|
||||||
|
for button_definition in row.iter() {
|
||||||
|
strip.cell(|ui| {
|
||||||
|
let sense = Sense::click();
|
||||||
|
|
||||||
|
let label = RichText::new(button_definition.value)
|
||||||
|
.font(DEFAULT_FONT)
|
||||||
|
.color(Color32::WHITE)
|
||||||
|
.background_color(Color32::BLACK);
|
||||||
|
|
||||||
|
ui.painter().rect_filled(
|
||||||
|
ui.available_rect_before_wrap(),
|
||||||
|
0.0,
|
||||||
|
Color32::GOLD,
|
||||||
|
);
|
||||||
|
ui.style_mut().spacing.window_margin = Margin {
|
||||||
|
left: 0.0,
|
||||||
|
right: 0.0,
|
||||||
|
top: 0.0,
|
||||||
|
bottom: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let max_size = {
|
||||||
|
let ret = ui.available_rect_before_wrap();
|
||||||
|
Vec2 {
|
||||||
|
x: ret.width(),
|
||||||
|
y: ret.height(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.add_enabled(
|
||||||
|
button_definition.enabled,
|
||||||
|
Button::new(label)
|
||||||
|
.stroke(Stroke::NONE)
|
||||||
|
.rounding(Rounding::none())
|
||||||
|
.fill(Color32::from_gray(0x12))
|
||||||
|
.min_size(max_size)
|
||||||
|
.sense(sense),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// let button = {
|
||||||
|
// let ret = ui.add_enabled(
|
||||||
|
// button_definition.enabled,
|
||||||
|
// Button::new(label)
|
||||||
|
// .stroke(Stroke::NONE)
|
||||||
|
// .rounding(Rounding::none())
|
||||||
|
// .fill(Color32::from_gray(0x12))
|
||||||
|
// .min_size(max_size)
|
||||||
|
// .sense(sense),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// ret
|
||||||
|
// };
|
||||||
|
|
||||||
|
// Check if any new touches intersect with this button
|
||||||
|
let max_rect = ui.max_rect();
|
||||||
|
for touch in self.new_touches.iter() {
|
||||||
|
if max_rect.contains(*touch) {
|
||||||
|
self.calculator
|
||||||
|
.calculator_input(button_definition.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout() -> Layout {
|
||||||
|
Layout::from_main_dir_and_cross_align(Direction::TopDown, Align::Center)
|
||||||
|
.with_cross_justify(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_input(&mut self, i: &egui::InputState) {
|
||||||
|
if i.events.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for e in i.events.iter() {
|
||||||
|
match e {
|
||||||
|
egui::Event::Text(t) => {
|
||||||
|
self.calculator.calculator_input(t.chars().next().unwrap());
|
||||||
|
}
|
||||||
|
// egui::Event::Key {
|
||||||
|
// key: Key::ArrowLeft,
|
||||||
|
// pressed: true,
|
||||||
|
// ..
|
||||||
|
// } => {
|
||||||
|
// self.calculator.calculator_input('<');
|
||||||
|
// }
|
||||||
|
egui::Event::Key {
|
||||||
|
key: Key::ArrowRight,
|
||||||
|
pressed: true,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
self.calculator.calculator_input('>');
|
||||||
|
}
|
||||||
|
egui::Event::Key {
|
||||||
|
key: Key::Enter,
|
||||||
|
pressed: true,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
self.calculator.calculator_input(' ');
|
||||||
|
}
|
||||||
|
egui::Event::Touch {
|
||||||
|
device_id: _,
|
||||||
|
id,
|
||||||
|
phase,
|
||||||
|
pos,
|
||||||
|
force: _,
|
||||||
|
} => {
|
||||||
|
self.handle_touch_event(phase.into(), pos.clone(), id);
|
||||||
|
}
|
||||||
|
egui::Event::PointerButton {
|
||||||
|
pos,
|
||||||
|
button,
|
||||||
|
pressed,
|
||||||
|
modifiers: _,
|
||||||
|
} => self.handle_touch_event(
|
||||||
|
if *pressed {
|
||||||
|
ClickPhase::Start
|
||||||
|
} else {
|
||||||
|
ClickPhase::End
|
||||||
|
},
|
||||||
|
*pos,
|
||||||
|
button,
|
||||||
|
),
|
||||||
|
|
||||||
|
egui::Event::Copy
|
||||||
|
| egui::Event::Cut
|
||||||
|
| egui::Event::Paste(_)
|
||||||
|
| egui::Event::Key {
|
||||||
|
key: _,
|
||||||
|
pressed: _,
|
||||||
|
repeat: _,
|
||||||
|
modifiers: _,
|
||||||
|
}
|
||||||
|
| egui::Event::PointerMoved(_)
|
||||||
|
| egui::Event::PointerGone
|
||||||
|
| egui::Event::Scroll(_)
|
||||||
|
| egui::Event::Zoom(_)
|
||||||
|
| egui::Event::CompositionStart
|
||||||
|
| egui::Event::CompositionUpdate(_)
|
||||||
|
| egui::Event::CompositionEnd(_)
|
||||||
|
| egui::Event::AccessKitActionRequest(_)
|
||||||
|
| egui::Event::MouseWheel {
|
||||||
|
unit: _,
|
||||||
|
delta: _,
|
||||||
|
modifiers: _,
|
||||||
|
}
|
||||||
|
| egui::Event::WindowFocused(_) => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_touch_event(&mut self, phase: ClickPhase, pos: Pos2, id: impl Into<ClickTapId>) {
|
||||||
|
let id = id.into();
|
||||||
|
match phase {
|
||||||
|
ClickPhase::Start => {
|
||||||
|
// TODO: This can be way better
|
||||||
|
|
||||||
|
// If this is a brand new touch
|
||||||
|
if !self.touches_down.contains(&id)
|
||||||
|
// And it isn't a duplicate
|
||||||
|
// This can occur on phones where touches can be pointer events
|
||||||
|
&& !self.new_touches.contains(&pos)
|
||||||
|
{
|
||||||
|
self.new_touches.push(pos);
|
||||||
|
}
|
||||||
|
self.touches_down.insert(id);
|
||||||
|
}
|
||||||
|
ClickPhase::End => {
|
||||||
|
self.touches_down.remove(&id);
|
||||||
|
}
|
||||||
|
ClickPhase::Ignored => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initialize_fonts(egui_ctx: &egui::Context) {
|
||||||
|
info!("Initializing fonts");
|
||||||
|
let mut fonts = FontDefinitions::default();
|
||||||
|
|
||||||
|
fonts.font_data.insert(
|
||||||
|
"LCD_Solid".to_owned(),
|
||||||
|
FontData::from_static(include_bytes!("../LcdSolid-VPzB.ttf")),
|
||||||
|
);
|
||||||
|
|
||||||
|
fonts
|
||||||
|
.families
|
||||||
|
.get_mut(&FontFamily::Monospace)
|
||||||
|
.unwrap()
|
||||||
|
.insert(0, "LCD_Solid".to_owned());
|
||||||
|
|
||||||
|
fonts
|
||||||
|
.families
|
||||||
|
.get_mut(&FontFamily::Monospace)
|
||||||
|
.unwrap()
|
||||||
|
.push("LCD_Solid".to_owned());
|
||||||
|
|
||||||
|
egui_ctx.set_fonts(fonts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl eframe::App for TemplateApp {
|
||||||
|
/// Called by the frame work to save state before shutdown.
|
||||||
|
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||||
|
eframe::set_value(storage, eframe::APP_KEY, self);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called each time the UI needs repainting, which may be many times per second.
|
||||||
|
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
|
||||||
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
|
let Self { .. } = self;
|
||||||
|
|
||||||
|
let mut style: Style = (*ctx.style()).clone();
|
||||||
|
style.spacing.button_padding = BUTTON_PADDING;
|
||||||
|
ctx.set_style(style);
|
||||||
|
|
||||||
|
self.calculator.error_state = ErrorState::default();
|
||||||
|
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
// ui.with_layout(self.layout, add_contents)
|
||||||
|
ui.input(|i: &egui::InputState| self.handle_input(i));
|
||||||
|
|
||||||
|
StripBuilder::new(ui)
|
||||||
|
// header
|
||||||
|
.size(Size::exact(65.0))
|
||||||
|
// Stack
|
||||||
|
// .size(Size::remainder().at_most(35.0))
|
||||||
|
.size(Size::remainder().at_least(40.0))
|
||||||
|
// Input
|
||||||
|
.size(Size::exact(40.0))
|
||||||
|
// Error
|
||||||
|
.size(Size::exact(20.0))
|
||||||
|
// Buttons
|
||||||
|
.size(Size::exact(450.0))
|
||||||
|
.vertical(|mut strip| {
|
||||||
|
strip.cell(|ui| {
|
||||||
|
ui.heading("rpn_rs_gui");
|
||||||
|
ui.hyperlink("https://gitea.austen-wares.com/stonewareslord/rpn_rs");
|
||||||
|
});
|
||||||
|
strip.cell(|ui| {
|
||||||
|
// Stack
|
||||||
|
ui.painter().rect_filled(
|
||||||
|
ui.available_rect_before_wrap(),
|
||||||
|
0.0,
|
||||||
|
Color32::LIGHT_GRAY,
|
||||||
|
);
|
||||||
|
self.draw_stack(ui);
|
||||||
|
});
|
||||||
|
strip.cell(|ui| {
|
||||||
|
self.draw_input(ui);
|
||||||
|
// Reset the error state and update `self.latest_error` if required
|
||||||
|
match std::mem::take(&mut self.calculator.error_state) {
|
||||||
|
ErrorState::NoModify => {}
|
||||||
|
ErrorState::Errored(e) => self.latest_error = Some(e),
|
||||||
|
ErrorState::Clear => self.latest_error = None,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
strip.cell(|ui| {
|
||||||
|
// Error bar
|
||||||
|
self.draw_error(ui);
|
||||||
|
});
|
||||||
|
strip.cell(|ui| {
|
||||||
|
// Buttons
|
||||||
|
self.draw_buttons(ui);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
self.new_touches.drain(..);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ErrorState {
|
||||||
|
// Do not touch the state of the error
|
||||||
|
NoModify,
|
||||||
|
// There was an error; this was the latest value
|
||||||
|
Errored(CalculatorError),
|
||||||
|
// We should clear the error at the end
|
||||||
|
Clear,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorState {
|
||||||
|
fn errored(&mut self, e: CalculatorError) {
|
||||||
|
error!("Calculator input error: {e:?}");
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Self::NoModify | ErrorState::Clear => *self = Self::Errored(e),
|
||||||
|
Self::Errored(_) => {
|
||||||
|
// We already errored, so do not change anything
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn success(&mut self) {
|
||||||
|
match self {
|
||||||
|
Self::NoModify => {
|
||||||
|
// There was a success and there was no previous failure, so clear any error value
|
||||||
|
*self = Self::Clear
|
||||||
|
}
|
||||||
|
Self::Errored(_) => {
|
||||||
|
// There was a previous error. We can't clear the error
|
||||||
|
}
|
||||||
|
Self::Clear => {
|
||||||
|
// The calculator error was already removed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Default for ErrorState {
|
||||||
|
fn default() -> ErrorState {
|
||||||
|
ErrorState::NoModify
|
||||||
|
}
|
||||||
|
}
|
108
rpn_rs_gui/src/app/buttons.rs
Normal file
108
rpn_rs_gui/src/app/buttons.rs
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
use rpn_rs::calc::Calculator;
|
||||||
|
|
||||||
|
pub struct CalculatorButton {
|
||||||
|
pub value: char,
|
||||||
|
pub help: Option<&'static str>,
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CalculatorButton {
|
||||||
|
const fn new(value: char, help: Option<&'static str>, enabled: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
value,
|
||||||
|
help,
|
||||||
|
enabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const BUTTON_LAYOUT_SETTINGS: &[&[CalculatorButton]] = &[
|
||||||
|
&[CalculatorButton::new('q', Some("Exit"), true)],
|
||||||
|
&[
|
||||||
|
CalculatorButton::new('d', Some("Degrees"), true),
|
||||||
|
CalculatorButton::new('r', Some("Radians"), true),
|
||||||
|
CalculatorButton::new('g', Some("Grads"), true),
|
||||||
|
],
|
||||||
|
&[
|
||||||
|
CalculatorButton::new('_', Some("Default"), true),
|
||||||
|
CalculatorButton::new(',', Some("Comma separated"), true),
|
||||||
|
CalculatorButton::new(' ', Some("Space separated"), true),
|
||||||
|
],
|
||||||
|
&[
|
||||||
|
CalculatorButton::new('s', Some("Scientific"), true),
|
||||||
|
CalculatorButton::new('S', Some("Scientific (stack precision)"), true),
|
||||||
|
],
|
||||||
|
&[
|
||||||
|
CalculatorButton::new('e', Some("Engineering"), true),
|
||||||
|
CalculatorButton::new('E', Some("Engineering (stack precision)"), true),
|
||||||
|
],
|
||||||
|
&[
|
||||||
|
CalculatorButton::new('f', Some("Fixed"), true),
|
||||||
|
CalculatorButton::new('F', Some("Fixed (stack precision)"), true),
|
||||||
|
],
|
||||||
|
// CalculatorButton::new('w', Some("Do not write settings and stack on quit (default)"), true),
|
||||||
|
// CalculatorButton::new('W', Some("Write stack and settings on quit"), true),
|
||||||
|
// CalculatorButton::new('L', Some("Left align"), true),
|
||||||
|
// CalculatorButton::new('R', Some("Right align"), true),
|
||||||
|
];
|
||||||
|
|
||||||
|
pub(crate) const BUTTON_LAYOUT: &[&[CalculatorButton]] = &[
|
||||||
|
&[
|
||||||
|
// CalculatorButton::new('s', "Sin", true),
|
||||||
|
CalculatorButton::new('\\', Some("Drop"), true),
|
||||||
|
// TODO: Settings buttons
|
||||||
|
CalculatorButton::new('@', Some("Settings"), true),
|
||||||
|
// CalculatorButton::new(' ', Some("Enter"), true),
|
||||||
|
CalculatorButton::new('>', Some("Swap"), true),
|
||||||
|
CalculatorButton::new('$', Some("Backspace"), true),
|
||||||
|
],
|
||||||
|
&[
|
||||||
|
// CalculatorButton::new('|', "AbsoluteValue", true),
|
||||||
|
CalculatorButton::new('^', Some("Pow"), true),
|
||||||
|
CalculatorButton::new('U', Some("Redo"), true),
|
||||||
|
CalculatorButton::new('u', Some("Undo"), true),
|
||||||
|
CalculatorButton::new('L', Some("Ln"), true),
|
||||||
|
// CalculatorButton::new('l', Some("Log"), true),
|
||||||
|
],
|
||||||
|
&[
|
||||||
|
// CalculatorButton::new('c', "Cos", true),
|
||||||
|
CalculatorButton::new('v', Some("Sqrt"), true),
|
||||||
|
// CalculatorButton::new('%', Some("Modulo"), true),
|
||||||
|
CalculatorButton::new('n', Some("Negate"), true),
|
||||||
|
CalculatorButton::new('i', Some("Inverse"), true),
|
||||||
|
CalculatorButton::new('/', Some("Divide"), true),
|
||||||
|
],
|
||||||
|
&[
|
||||||
|
// CalculatorButton::new('t', "Tan", true),
|
||||||
|
CalculatorButton::new('7', None, true),
|
||||||
|
CalculatorButton::new('8', None, true),
|
||||||
|
CalculatorButton::new('9', None, true),
|
||||||
|
CalculatorButton::new('*', Some("Multiply"), true),
|
||||||
|
],
|
||||||
|
&[
|
||||||
|
// CalculatorButton::new('S', "ASin", true),
|
||||||
|
CalculatorButton::new('4', None, true),
|
||||||
|
CalculatorButton::new('5', None, true),
|
||||||
|
CalculatorButton::new('6', None, true),
|
||||||
|
CalculatorButton::new('-', Some("Subtract"), true),
|
||||||
|
],
|
||||||
|
&[
|
||||||
|
// CalculatorButton::new('C', "ACos", true),
|
||||||
|
CalculatorButton::new('1', None, true),
|
||||||
|
CalculatorButton::new('2', None, true),
|
||||||
|
CalculatorButton::new('3', None, true),
|
||||||
|
CalculatorButton::new('+', Some("Add"), true),
|
||||||
|
],
|
||||||
|
&[
|
||||||
|
// CalculatorButton::new('T', "ATan", true),
|
||||||
|
CalculatorButton::new('0', None, true),
|
||||||
|
CalculatorButton::new('.', Some("Decimal"), true),
|
||||||
|
CalculatorButton::new(' ', Some("Return"), true),
|
||||||
|
CalculatorButton::new(' ', Some("Return"), true),
|
||||||
|
],
|
||||||
|
// CalculatorButton::new ( '?', "IntegerDivide", true),
|
||||||
|
// CalculatorButton::new ( 'V', "BuildVector", true),
|
||||||
|
// CalculatorButton::new ( 'M', "BuildMatrix", true),
|
||||||
|
// CalculatorButton::new ( '_', "Deconstruct", true),
|
||||||
|
// CalculatorButton::new ( ', true)', "Transpose"),
|
||||||
|
];
|
4
rpn_rs_gui/src/lib.rs
Normal file
4
rpn_rs_gui/src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
#![warn(clippy::all, rust_2018_idioms)]
|
||||||
|
|
||||||
|
mod app;
|
||||||
|
pub use app::TemplateApp;
|
39
rpn_rs_gui/src/main.rs
Normal file
39
rpn_rs_gui/src/main.rs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#![warn(clippy::all, rust_2018_idioms)]
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||||
|
|
||||||
|
// When compiling natively:
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn main() -> eframe::Result<()> {
|
||||||
|
// Log to stdout (if you run with `RUST_LOG=debug`).
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
let native_options = eframe::NativeOptions::default();
|
||||||
|
eframe::run_native(
|
||||||
|
"RPN RS2",
|
||||||
|
native_options,
|
||||||
|
Box::new(|cc| Box::new(rpn_rs_gui::TemplateApp::new(cc))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// when compiling to web using trunk.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn main() {
|
||||||
|
// Make sure panics are logged using `console.error`.
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
|
// Redirect tracing to console.log and friends:
|
||||||
|
tracing_wasm::set_as_global_default();
|
||||||
|
|
||||||
|
let web_options = eframe::WebOptions::default();
|
||||||
|
|
||||||
|
wasm_bindgen_futures::spawn_local(async {
|
||||||
|
eframe::WebRunner::new()
|
||||||
|
.start(
|
||||||
|
"the_canvas_id", // hardcode it
|
||||||
|
web_options,
|
||||||
|
Box::new(|cc| Box::new(rpn_rs_gui::TemplateApp::new(cc))),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("failed to start eframe");
|
||||||
|
});
|
||||||
|
}
|
14
rpn_rs_tui/Cargo.toml
Normal file
14
rpn_rs_tui/Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "rpn_rs_tui"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rpn_rs = {path=".."}
|
||||||
|
crossterm = { version = "0.18" }
|
||||||
|
tui = { version = "0.14", default-features = false, features = ["crossterm"] }
|
||||||
|
tracing-appender = "0.2.2"
|
||||||
|
tracing-subscriber = "0.3.17"
|
||||||
|
tracing = "0.1.37"
|
@ -5,8 +5,9 @@
|
|||||||
// Cannot fix this, so don't warn me about it
|
// Cannot fix this, so don't warn me about it
|
||||||
#![allow(clippy::multiple_crate_versions)]
|
#![allow(clippy::multiple_crate_versions)]
|
||||||
|
|
||||||
mod calc;
|
use rpn_rs::{calc, constants};
|
||||||
mod event;
|
mod event;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
const BORDER_SIZE: u16 = 2;
|
const BORDER_SIZE: u16 = 2;
|
||||||
const HELP_TEXT: &str = "\
|
const HELP_TEXT: &str = "\
|
||||||
@ -61,7 +62,7 @@ use crossterm::{
|
|||||||
use event::{Event, Events};
|
use event::{Event, Events};
|
||||||
// use io::stdout;
|
// use io::stdout;
|
||||||
|
|
||||||
use std::{cmp, convert::TryFrom, error::Error, io, io::Write};
|
use std::{cmp, convert::TryFrom, error::Error, fs::OpenOptions, io, io::Write};
|
||||||
use tui::{
|
use tui::{
|
||||||
backend::CrosstermBackend,
|
backend::CrosstermBackend,
|
||||||
layout::{Constraint, Direction, Layout, Rect},
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
@ -308,7 +309,36 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn init_logging() {
|
||||||
|
use tracing_subscriber::{filter::LevelFilter, fmt, prelude::*, EnvFilter};
|
||||||
|
|
||||||
|
let file = OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open("/tmp/rpn_rs.log")
|
||||||
|
.unwrap();
|
||||||
|
let (non_blocking_file, _guard) = tracing_appender::non_blocking(file);
|
||||||
|
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(fmt::layer().with_writer(non_blocking_file))
|
||||||
|
// .with(fmt::layer().with_writer(io::stderr))
|
||||||
|
.with(
|
||||||
|
EnvFilter::builder()
|
||||||
|
.with_default_directive(LevelFilter::ERROR.into())
|
||||||
|
.with_env_var("RPN_RS_LOG")
|
||||||
|
.from_env()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
info!("Logging initialized");
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
if std::env::var("ENABLE_LOGGING").as_ref().map(String::as_str) == Ok("1") {
|
||||||
|
init_logging();
|
||||||
|
}
|
||||||
|
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
|
|
||||||
let mut stdout = io::stdout();
|
let mut stdout = io::stdout();
|
||||||
@ -380,6 +410,10 @@ fn handle_key(app: &mut App, key: KeyEvent) -> CalculatorResult<CalculatorRespon
|
|||||||
code: KeyCode::Enter,
|
code: KeyCode::Enter,
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
}
|
}
|
||||||
|
| KeyEvent {
|
||||||
|
code: KeyCode::Char('j'),
|
||||||
|
modifiers: KeyModifiers::CONTROL,
|
||||||
|
}
|
||||||
| KeyEvent {
|
| KeyEvent {
|
||||||
code: KeyCode::Char(' '),
|
code: KeyCode::Char(' '),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
@ -418,7 +452,7 @@ fn handle_key(app: &mut App, key: KeyEvent) -> CalculatorResult<CalculatorRespon
|
|||||||
app.calculator.take_input(c)?;
|
app.calculator.take_input(c)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_key_event => {}
|
||||||
},
|
},
|
||||||
(AppState::Help, _) => match key {
|
(AppState::Help, _) => match key {
|
||||||
KeyEvent {
|
KeyEvent {
|
25
src/calc.rs
25
src/calc.rs
@ -1,9 +1,12 @@
|
|||||||
|
use rust_decimal::prelude::ToPrimitive;
|
||||||
pub mod entries;
|
pub mod entries;
|
||||||
|
use crate::constants;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod operations;
|
pub mod operations;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
use crate::calc::entries::CalculatorEntry;
|
use crate::calc::entries::CalculatorEntry;
|
||||||
|
use rust_decimal::Decimal;
|
||||||
|
|
||||||
use confy::{load, store};
|
use confy::{load, store};
|
||||||
use entries::{Entry, Matrix, Number, Vector};
|
use entries::{Entry, Matrix, Number, Vector};
|
||||||
@ -197,7 +200,7 @@ impl Default for Calculator {
|
|||||||
CalculatorConstant {
|
CalculatorConstant {
|
||||||
help: String::from("Tau (2pi)"),
|
help: String::from("Tau (2pi)"),
|
||||||
value: Entry::Number(Number {
|
value: Entry::Number(Number {
|
||||||
value: std::f64::consts::TAU,
|
value: constants::TAU,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -206,7 +209,7 @@ impl Default for Calculator {
|
|||||||
CalculatorConstant {
|
CalculatorConstant {
|
||||||
help: String::from("Euler's Number e"),
|
help: String::from("Euler's Number e"),
|
||||||
value: Entry::Number(Number {
|
value: Entry::Number(Number {
|
||||||
value: std::f64::consts::E,
|
value: constants::E,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -215,7 +218,7 @@ impl Default for Calculator {
|
|||||||
CalculatorConstant {
|
CalculatorConstant {
|
||||||
help: String::from("Pi"),
|
help: String::from("Pi"),
|
||||||
value: Entry::Number(Number {
|
value: Entry::Number(Number {
|
||||||
value: std::f64::consts::PI,
|
value: constants::PI,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -479,7 +482,10 @@ impl Calculator {
|
|||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
let f = self.l.parse::<f64>().or(Err(CalculatorError::ParseError))?;
|
let f = self
|
||||||
|
.l
|
||||||
|
.parse::<Decimal>()
|
||||||
|
.or(Err(CalculatorError::ParseError))?;
|
||||||
self.push(Entry::Number(Number { value: f }))?;
|
self.push(Entry::Number(Number { value: f }))?;
|
||||||
self.l.clear();
|
self.l.clear();
|
||||||
Ok(true)
|
Ok(true)
|
||||||
@ -520,14 +526,11 @@ impl Calculator {
|
|||||||
Entry::Matrix(_) | Entry::Vector(_) => return Err(CalculatorError::TypeMismatch),
|
Entry::Matrix(_) | Entry::Vector(_) => return Err(CalculatorError::TypeMismatch),
|
||||||
Entry::Number(Number { value }) => value,
|
Entry::Number(Number { value }) => value,
|
||||||
};
|
};
|
||||||
// Ensure this can be cast to a usize
|
|
||||||
if !f.is_finite() || f.is_sign_negative() {
|
|
||||||
return Err(CalculatorError::ArithmeticError);
|
|
||||||
}
|
|
||||||
#[allow(clippy::cast_sign_loss)]
|
|
||||||
let u = f as usize;
|
|
||||||
|
|
||||||
Ok((u, entry))
|
match f.to_usize() {
|
||||||
|
Some(u) => Ok((u, entry)),
|
||||||
|
None => Err(CalculatorError::ArithmeticError),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/// Pops a precision instead of an Entry. Precisions are of type usize
|
/// Pops a precision instead of an Entry. Precisions are of type usize
|
||||||
pub fn pop_precision(&mut self) -> CalculatorResult<usize> {
|
pub fn pop_precision(&mut self) -> CalculatorResult<usize> {
|
||||||
|
@ -255,6 +255,8 @@ impl fmt::Display for Entry {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use rust_decimal::Decimal;
|
||||||
|
use rust_decimal_macros::dec;
|
||||||
|
|
||||||
fn valid_square_matrix() -> Entry {
|
fn valid_square_matrix() -> Entry {
|
||||||
Entry::Matrix(Matrix {
|
Entry::Matrix(Matrix {
|
||||||
@ -266,25 +268,25 @@ mod tests {
|
|||||||
Vector {
|
Vector {
|
||||||
direction: VectorDirection::Column,
|
direction: VectorDirection::Column,
|
||||||
values: vec![
|
values: vec![
|
||||||
Number { value: 1.0_f64 },
|
Number { value: dec!(1.0) },
|
||||||
Number { value: 2.0_f64 },
|
Number { value: dec!(2.0) },
|
||||||
Number { value: -3.0_f64 },
|
Number { value: dec!(-3.0) },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
Vector {
|
Vector {
|
||||||
direction: VectorDirection::Column,
|
direction: VectorDirection::Column,
|
||||||
values: vec![
|
values: vec![
|
||||||
Number { value: 4.0_f64 },
|
Number { value: dec!(4.0) },
|
||||||
Number { value: -5.0_f64 },
|
Number { value: dec!(-5.0) },
|
||||||
Number { value: 0.0_f64 },
|
Number { value: dec!(0.0) },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
Vector {
|
Vector {
|
||||||
direction: VectorDirection::Column,
|
direction: VectorDirection::Column,
|
||||||
values: vec![
|
values: vec![
|
||||||
Number { value: -7.0_f64 },
|
Number { value: dec!(-7.0) },
|
||||||
Number { value: 8.0_f64 },
|
Number { value: dec!(8.0) },
|
||||||
Number { value: 9.0_f64 },
|
Number { value: dec!(9.0) },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -301,25 +303,25 @@ mod tests {
|
|||||||
Entry::Vector(Vector {
|
Entry::Vector(Vector {
|
||||||
direction: VectorDirection::Column,
|
direction: VectorDirection::Column,
|
||||||
values: vec![
|
values: vec![
|
||||||
Number { value: -1.0_f64 },
|
Number { value: dec!(-1.0) },
|
||||||
Number { value: -2.0_f64 },
|
Number { value: dec!(-2.0) },
|
||||||
Number { value: 3.0_f64 },
|
Number { value: dec!(3.0) },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
Entry::Vector(Vector {
|
Entry::Vector(Vector {
|
||||||
direction: VectorDirection::Column,
|
direction: VectorDirection::Column,
|
||||||
values: vec![
|
values: vec![
|
||||||
Number { value: -4.0_f64 },
|
Number { value: dec!(-4.0) },
|
||||||
Number { value: 5.0_f64 },
|
Number { value: dec!(5.0) },
|
||||||
Number { value: -0.0_f64 },
|
Number { value: dec!(-0.0) },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
Entry::Vector(Vector {
|
Entry::Vector(Vector {
|
||||||
direction: VectorDirection::Column,
|
direction: VectorDirection::Column,
|
||||||
values: vec![
|
values: vec![
|
||||||
Number { value: 7.0_f64 },
|
Number { value: dec!(7.0) },
|
||||||
Number { value: -8.0_f64 },
|
Number { value: dec!(-8.0) },
|
||||||
Number { value: -9.0_f64 },
|
Number { value: dec!(-9.0) },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
@ -331,25 +333,25 @@ mod tests {
|
|||||||
// Entry::Vector(Vector {
|
// Entry::Vector(Vector {
|
||||||
// direction: VectorDirection::Column,
|
// direction: VectorDirection::Column,
|
||||||
// values: vec![
|
// values: vec![
|
||||||
// Number { value: 1.0_f64 },
|
// Number { value: dec!(1.0) },
|
||||||
// Number { value: 2.0_f64 },
|
// Number { value: dec!(2.0) },
|
||||||
// Number { value: 3.0_f64 },
|
// Number { value: dec!(3.0) },
|
||||||
// ],
|
// ],
|
||||||
// }),
|
// }),
|
||||||
// Entry::Vector(Vector {
|
// Entry::Vector(Vector {
|
||||||
// direction: VectorDirection::Column,
|
// direction: VectorDirection::Column,
|
||||||
// values: vec![
|
// values: vec![
|
||||||
// Number { value: 4.0_f64 },
|
// Number { value: dec!(4.0) },
|
||||||
// Number { value: 5.0_f64 },
|
// Number { value: dec!(5.0) },
|
||||||
// Number { value: 0.0_f64 },
|
// Number { value: dec!(0.0) },
|
||||||
// ],
|
// ],
|
||||||
// }),
|
// }),
|
||||||
// Entry::Vector(Vector {
|
// Entry::Vector(Vector {
|
||||||
// direction: VectorDirection::Column,
|
// direction: VectorDirection::Column,
|
||||||
// values: vec![
|
// values: vec![
|
||||||
// Number { value: 7.0_f64 },
|
// Number { value: dec!(7.0) },
|
||||||
// Number { value: 8.0_f64 },
|
// Number { value: dec!(8.0) },
|
||||||
// Number { value: 9.0_f64 },
|
// Number { value: dec!(9.0) },
|
||||||
// ],
|
// ],
|
||||||
// }),
|
// }),
|
||||||
// ]),
|
// ]),
|
||||||
@ -358,23 +360,23 @@ mod tests {
|
|||||||
// Entry::Vector(Vector {
|
// Entry::Vector(Vector {
|
||||||
// direction: VectorDirection::Column,
|
// direction: VectorDirection::Column,
|
||||||
// values: vec![
|
// values: vec![
|
||||||
// Number{value: 1.0_f64},
|
// Number{value: dec!(1.0},)
|
||||||
// Number{value: 2.0_f64},
|
// Number{value: dec!(2.0},)
|
||||||
// Number{value: -3.0_f64},
|
// Number{value: dec!(-3.0},)
|
||||||
// ]}),
|
// ]}),
|
||||||
// Entry::Vector (Vector{
|
// Entry::Vector (Vector{
|
||||||
// direction: VectorDirection::Column,
|
// direction: VectorDirection::Column,
|
||||||
// values: vec![
|
// values: vec![
|
||||||
// Number{value: 4.0_f64},
|
// Number{value: dec!(4.0},)
|
||||||
// Number{value: -5.0_f64},
|
// Number{value: dec!(-5.0},)
|
||||||
// Number{value: 0.0_f64},
|
// Number{value: dec!(0.0},)
|
||||||
// ]}),
|
// ]}),
|
||||||
// Entry::Vector (Vector{
|
// Entry::Vector (Vector{
|
||||||
// direction: VectorDirection::Column,
|
// direction: VectorDirection::Column,
|
||||||
// values: vec![
|
// values: vec![
|
||||||
// Number{value: -7.0_f64},
|
// Number{value: dec!(-7.0},)
|
||||||
// Number{value: 8.0_f64},
|
// Number{value: dec!(8.0},)
|
||||||
// Number{value: 9.0_f64},
|
// Number{value: dec!(9.0},)
|
||||||
// ])),
|
// ])),
|
||||||
(
|
(
|
||||||
"transpose",
|
"transpose",
|
||||||
@ -383,25 +385,25 @@ mod tests {
|
|||||||
Entry::Vector(Vector {
|
Entry::Vector(Vector {
|
||||||
direction: VectorDirection::Column,
|
direction: VectorDirection::Column,
|
||||||
values: vec![
|
values: vec![
|
||||||
Number { value: 1.0_f64 },
|
Number { value: dec!(1.0) },
|
||||||
Number { value: 4.0_f64 },
|
Number { value: dec!(4.0) },
|
||||||
Number { value: -7.0_f64 },
|
Number { value: dec!(-7.0) },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
Entry::Vector(Vector {
|
Entry::Vector(Vector {
|
||||||
direction: VectorDirection::Column,
|
direction: VectorDirection::Column,
|
||||||
values: vec![
|
values: vec![
|
||||||
Number { value: 2.0_f64 },
|
Number { value: dec!(2.0) },
|
||||||
Number { value: -5.0_f64 },
|
Number { value: dec!(-5.0) },
|
||||||
Number { value: 8.0_f64 },
|
Number { value: dec!(8.0) },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
Entry::Vector(Vector {
|
Entry::Vector(Vector {
|
||||||
direction: VectorDirection::Column,
|
direction: VectorDirection::Column,
|
||||||
values: vec![
|
values: vec![
|
||||||
Number { value: -3.0_f64 },
|
Number { value: dec!(-3.0) },
|
||||||
Number { value: 0.0_f64 },
|
Number { value: dec!(0.0) },
|
||||||
Number { value: 9.0_f64 },
|
Number { value: dec!(9.0) },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
@ -416,9 +418,9 @@ mod tests {
|
|||||||
vectors: vec![Vector {
|
vectors: vec![Vector {
|
||||||
direction: VectorDirection::Column,
|
direction: VectorDirection::Column,
|
||||||
values: vec![
|
values: vec![
|
||||||
Number { value: 1.0_f64 },
|
Number { value: dec!(1.0) },
|
||||||
Number { value: 100.0_f64 },
|
Number { value: dec!(100.0) },
|
||||||
Number { value: 64.0_f64 },
|
Number { value: dec!(64.0) },
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
})
|
})
|
||||||
@ -426,9 +428,9 @@ mod tests {
|
|||||||
Matrix::from(&[Entry::Vector(Vector {
|
Matrix::from(&[Entry::Vector(Vector {
|
||||||
direction: VectorDirection::Column,
|
direction: VectorDirection::Column,
|
||||||
values: vec![
|
values: vec![
|
||||||
Number { value: 1.0_f64 },
|
Number { value: dec!(1.0) },
|
||||||
Number { value: 10.0_f64 },
|
Number { value: dec!(10.0) },
|
||||||
Number { value: 8.0_f64 },
|
Number { value: dec!(8.0) },
|
||||||
],
|
],
|
||||||
})]),
|
})]),
|
||||||
),
|
),
|
||||||
@ -443,10 +445,10 @@ mod tests {
|
|||||||
vectors: vec![Vector {
|
vectors: vec![Vector {
|
||||||
direction: VectorDirection::Column,
|
direction: VectorDirection::Column,
|
||||||
values: vec![
|
values: vec![
|
||||||
Number { value: 1.0_f64 },
|
Number { value: dec!(1.0) },
|
||||||
Number { value: 100.0_f64 },
|
Number { value: dec!(100.0) },
|
||||||
Number {
|
Number {
|
||||||
value: 100_000.0_f64,
|
value: dec!(100_000.0),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
@ -455,9 +457,9 @@ mod tests {
|
|||||||
Matrix::from(&[Entry::Vector(Vector {
|
Matrix::from(&[Entry::Vector(Vector {
|
||||||
direction: VectorDirection::Column,
|
direction: VectorDirection::Column,
|
||||||
values: vec![
|
values: vec![
|
||||||
Number { value: 0.0_f64 },
|
Number { value: dec!(0.0) },
|
||||||
Number { value: 2.0_f64 },
|
Number { value: dec!(2.0) },
|
||||||
Number { value: 5.0_f64 },
|
Number { value: dec!(5.0) },
|
||||||
],
|
],
|
||||||
})]),
|
})]),
|
||||||
),
|
),
|
||||||
@ -471,9 +473,9 @@ mod tests {
|
|||||||
vectors: vec![Vector {
|
vectors: vec![Vector {
|
||||||
direction: VectorDirection::Column,
|
direction: VectorDirection::Column,
|
||||||
values: vec![
|
values: vec![
|
||||||
Number { value: 1.0_f64 },
|
Number { value: dec!(1.0) },
|
||||||
Number {
|
Number {
|
||||||
value: std::f64::consts::E,
|
value: constants::E,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
@ -481,7 +483,7 @@ mod tests {
|
|||||||
.ln(),
|
.ln(),
|
||||||
Matrix::from(&[Entry::Vector(Vector {
|
Matrix::from(&[Entry::Vector(Vector {
|
||||||
direction: VectorDirection::Column,
|
direction: VectorDirection::Column,
|
||||||
values: vec![Number { value: 0.0_f64 }, Number { value: 1.0_f64 }],
|
values: vec![Number { value: dec!(0.0) }, Number { value: dec!(1.0) }],
|
||||||
})]),
|
})]),
|
||||||
),
|
),
|
||||||
] {
|
] {
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
use super::{Entry, Number, Vector, VectorDirection};
|
use super::{Entry, Number, Vector, VectorDirection};
|
||||||
use crate::{
|
use crate::calc::{
|
||||||
calc::{
|
entries::CalculatorEntry,
|
||||||
errors::{CalculatorError, CalculatorResult},
|
errors::{CalculatorError, CalculatorResult},
|
||||||
types::CalculatorAngleMode,
|
types::{CalculatorAngleMode, CalculatorDisplayMode},
|
||||||
CalculatorDisplayMode,
|
|
||||||
},
|
|
||||||
CalculatorEntry,
|
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
@ -1,37 +1,27 @@
|
|||||||
use super::{Entry, Matrix, Vector};
|
use super::{Entry, Matrix, Vector};
|
||||||
use crate::{
|
use crate::{
|
||||||
calc::{
|
calc::{
|
||||||
|
entries::CalculatorEntry,
|
||||||
errors::{CalculatorError, CalculatorResult},
|
errors::{CalculatorError, CalculatorResult},
|
||||||
types::CalculatorAngleMode,
|
types::{CalculatorAngleMode, CalculatorDisplayMode},
|
||||||
CalculatorDisplayMode,
|
|
||||||
},
|
},
|
||||||
CalculatorEntry,
|
constants,
|
||||||
};
|
};
|
||||||
|
use rust_decimal::{Decimal, MathematicalOps};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub struct Number {
|
pub struct Number {
|
||||||
pub value: f64,
|
pub value: Decimal,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for Number {
|
// impl PartialEq for Number {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
// fn eq(&self, other: &Self) -> bool {
|
||||||
if self.value.is_nan() && other.value.is_nan()
|
// (self.value - other.value).abs() < f64::EPSILON
|
||||||
|| self.value.is_infinite() && other.value.is_infinite()
|
// }
|
||||||
{
|
// }
|
||||||
true
|
// }
|
||||||
} else if self.value.is_nan()
|
|
||||||
|| self.value.is_infinite()
|
|
||||||
|| other.value.is_infinite()
|
|
||||||
|| other.value.is_nan()
|
|
||||||
{
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
(self.value - other.value).abs() < f64::EPSILON
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CalculatorEntry for Number {
|
impl CalculatorEntry for Number {
|
||||||
fn to_editable_string(&self) -> CalculatorResult<String> {
|
fn to_editable_string(&self) -> CalculatorResult<String> {
|
||||||
@ -49,7 +39,7 @@ impl CalculatorEntry for Number {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn is_valid(&self) -> bool {
|
fn is_valid(&self) -> bool {
|
||||||
!self.value.is_nan() && !self.value.is_infinite()
|
true
|
||||||
}
|
}
|
||||||
fn validate(self) -> CalculatorResult<Entry> {
|
fn validate(self) -> CalculatorResult<Entry> {
|
||||||
if self.is_valid() {
|
if self.is_valid() {
|
||||||
@ -69,7 +59,9 @@ impl CalculatorEntry for Number {
|
|||||||
}
|
}
|
||||||
fn inverse(&self) -> CalculatorResult<Entry> {
|
fn inverse(&self) -> CalculatorResult<Entry> {
|
||||||
Self {
|
Self {
|
||||||
value: self.value.recip(),
|
value: constants::ONE
|
||||||
|
.checked_div(self.value)
|
||||||
|
.ok_or(CalculatorError::ArithmeticError)?,
|
||||||
}
|
}
|
||||||
.validate()
|
.validate()
|
||||||
}
|
}
|
||||||
@ -79,70 +71,103 @@ impl CalculatorEntry for Number {
|
|||||||
fn sin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
|
fn sin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
|
||||||
Ok(Entry::Number(Self {
|
Ok(Entry::Number(Self {
|
||||||
value: match angle_mode {
|
value: match angle_mode {
|
||||||
CalculatorAngleMode::Degrees => self.value.to_radians().sin(),
|
CalculatorAngleMode::Degrees => (self.value * constants::DEC_TO_RAD_MULTIPLIER)
|
||||||
CalculatorAngleMode::Radians => self.value.sin(),
|
.checked_sin()
|
||||||
CalculatorAngleMode::Grads => (self.value * std::f64::consts::PI / 200.0).sin(),
|
.ok_or(CalculatorError::ArithmeticError)?,
|
||||||
|
CalculatorAngleMode::Radians => self
|
||||||
|
.value
|
||||||
|
.checked_sin()
|
||||||
|
.ok_or(CalculatorError::ArithmeticError)?,
|
||||||
|
CalculatorAngleMode::Grads => (self.value * constants::GRAD_TO_RAD_MULTIPLIER)
|
||||||
|
.checked_sin()
|
||||||
|
.ok_or(CalculatorError::ArithmeticError)?,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
fn cos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
|
fn cos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
|
||||||
Ok(Entry::Number(Self {
|
Ok(Entry::Number(Self {
|
||||||
value: match angle_mode {
|
value: match angle_mode {
|
||||||
CalculatorAngleMode::Degrees => self.value.to_radians().cos(),
|
CalculatorAngleMode::Degrees => (self.value * constants::DEC_TO_RAD_MULTIPLIER)
|
||||||
CalculatorAngleMode::Radians => self.value.cos(),
|
.checked_cos()
|
||||||
CalculatorAngleMode::Grads => (self.value * std::f64::consts::PI / 200.0).cos(),
|
.ok_or(CalculatorError::ArithmeticError)?,
|
||||||
|
CalculatorAngleMode::Radians => self
|
||||||
|
.value
|
||||||
|
.checked_cos()
|
||||||
|
.ok_or(CalculatorError::ArithmeticError)?,
|
||||||
|
CalculatorAngleMode::Grads => (self.value * constants::GRAD_TO_RAD_MULTIPLIER)
|
||||||
|
.checked_cos()
|
||||||
|
.ok_or(CalculatorError::ArithmeticError)?,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
fn tan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
|
fn tan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
|
||||||
Ok(Entry::Number(Self {
|
Ok(Entry::Number(Self {
|
||||||
value: match angle_mode {
|
value: match angle_mode {
|
||||||
CalculatorAngleMode::Degrees => self.value.to_radians().tan(),
|
CalculatorAngleMode::Degrees => (self.value * constants::DEC_TO_RAD_MULTIPLIER)
|
||||||
CalculatorAngleMode::Radians => self.value.tan(),
|
.checked_tan()
|
||||||
CalculatorAngleMode::Grads => (self.value * std::f64::consts::PI / 200.0).tan(),
|
.ok_or(CalculatorError::ArithmeticError)?,
|
||||||
|
CalculatorAngleMode::Radians => self
|
||||||
|
.value
|
||||||
|
.checked_tan()
|
||||||
|
.ok_or(CalculatorError::ArithmeticError)?,
|
||||||
|
CalculatorAngleMode::Grads => (self.value * constants::GRAD_TO_RAD_MULTIPLIER)
|
||||||
|
.checked_tan()
|
||||||
|
.ok_or(CalculatorError::ArithmeticError)?,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
fn asin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
|
fn asin(&self, _angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
|
||||||
Ok(Entry::Number(Self {
|
// TODO: Implement this
|
||||||
value: match angle_mode {
|
Err(CalculatorError::NotYetImplemented)
|
||||||
CalculatorAngleMode::Degrees => self.value.asin().to_degrees(),
|
// Ok(Entry::Number(Self {
|
||||||
CalculatorAngleMode::Radians => self.value.asin(),
|
// value: match angle_mode {
|
||||||
CalculatorAngleMode::Grads => self.value.asin() * 200.0 / std::f64::consts::PI,
|
// CalculatorAngleMode::Degrees => self.value.asin().to_degrees(),
|
||||||
},
|
// CalculatorAngleMode::Radians => self.value.asin(),
|
||||||
}))
|
// CalculatorAngleMode::Grads => self.value.asin() * 200.0 / std::f64::consts::PI,
|
||||||
|
// },
|
||||||
|
// }))
|
||||||
}
|
}
|
||||||
fn acos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
|
fn acos(&self, _angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
|
||||||
Ok(Entry::Number(Self {
|
// TODO: Implement this
|
||||||
value: match angle_mode {
|
Err(CalculatorError::NotYetImplemented)
|
||||||
CalculatorAngleMode::Degrees => self.value.acos().to_degrees(),
|
// Ok(Entry::Number(Self {
|
||||||
CalculatorAngleMode::Radians => self.value.acos(),
|
// value: match angle_mode {
|
||||||
CalculatorAngleMode::Grads => self.value.acos() * 200.0 / std::f64::consts::PI,
|
// CalculatorAngleMode::Degrees => self.value.acos().to_degrees(),
|
||||||
},
|
// CalculatorAngleMode::Radians => self.value.acos(),
|
||||||
}))
|
// CalculatorAngleMode::Grads => self.value.acos() * 200.0 / std::f64::consts::PI,
|
||||||
|
// },
|
||||||
|
// }))
|
||||||
}
|
}
|
||||||
fn atan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
|
fn atan(&self, _angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
|
||||||
Ok(Entry::Number(Self {
|
// TODO: Implement this
|
||||||
value: match angle_mode {
|
Err(CalculatorError::NotYetImplemented)
|
||||||
CalculatorAngleMode::Degrees => self.value.atan().to_degrees(),
|
// Ok(Entry::Number(Self {
|
||||||
CalculatorAngleMode::Radians => self.value.atan(),
|
// value: match angle_mode {
|
||||||
CalculatorAngleMode::Grads => self.value.atan() * 200.0 / std::f64::consts::PI,
|
// CalculatorAngleMode::Degrees => self.value.atan().to_degrees(),
|
||||||
},
|
// CalculatorAngleMode::Radians => self.value.atan(),
|
||||||
}))
|
// CalculatorAngleMode::Grads => self.value.atan() * 200.0 / std::f64::consts::PI,
|
||||||
|
// },
|
||||||
|
// }))
|
||||||
}
|
}
|
||||||
fn sqrt(&self) -> CalculatorResult<Entry> {
|
fn sqrt(&self) -> CalculatorResult<Entry> {
|
||||||
Ok(Entry::Number(Self {
|
Ok(Entry::Number(Self {
|
||||||
value: self.value.sqrt(),
|
value: self.value.sqrt().ok_or(CalculatorError::ArithmeticError)?,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
fn log(&self) -> CalculatorResult<Entry> {
|
fn log(&self) -> CalculatorResult<Entry> {
|
||||||
Ok(Entry::Number(Self {
|
Ok(Entry::Number(Self {
|
||||||
value: self.value.log10(),
|
value: self
|
||||||
|
.value
|
||||||
|
.checked_log10()
|
||||||
|
.ok_or(CalculatorError::ArithmeticError)?,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
fn ln(&self) -> CalculatorResult<Entry> {
|
fn ln(&self) -> CalculatorResult<Entry> {
|
||||||
Ok(Entry::Number(Self {
|
Ok(Entry::Number(Self {
|
||||||
value: self.value.ln(),
|
value: self
|
||||||
|
.value
|
||||||
|
.checked_ln()
|
||||||
|
.ok_or(CalculatorError::ArithmeticError)?,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,8 +215,20 @@ impl CalculatorEntry for Number {
|
|||||||
match arg {
|
match arg {
|
||||||
Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::int_divide),
|
Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::int_divide),
|
||||||
Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::int_divide),
|
Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::int_divide),
|
||||||
Entry::Number(number) => Self {
|
Entry::Number(number) => {
|
||||||
value: self.value.div_euclid(number.value),
|
let q = (self.value / number.value).trunc();
|
||||||
|
Self {
|
||||||
|
// Implementation based on https://doc.rust-lang.org/src/std/f64.rs.html#263-269
|
||||||
|
value: if self.value % number.value < constants::ZERO {
|
||||||
|
if number.value > constants::ZERO {
|
||||||
|
q - constants::ONE
|
||||||
|
} else {
|
||||||
|
q + constants::ONE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
q
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.validate(),
|
.validate(),
|
||||||
}
|
}
|
||||||
@ -211,7 +248,10 @@ impl CalculatorEntry for Number {
|
|||||||
Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::pow),
|
Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::pow),
|
||||||
Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::pow),
|
Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::pow),
|
||||||
Entry::Number(number) => Self {
|
Entry::Number(number) => Self {
|
||||||
value: self.value.powf(number.value),
|
value: self
|
||||||
|
.value
|
||||||
|
.checked_powd(number.value)
|
||||||
|
.ok_or(CalculatorError::ArithmeticError)?,
|
||||||
}
|
}
|
||||||
.validate(),
|
.validate(),
|
||||||
}
|
}
|
||||||
@ -219,7 +259,9 @@ impl CalculatorEntry for Number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Number {
|
impl Number {
|
||||||
pub const ZERO: Self = Self { value: 0.0_f64 };
|
pub const ZERO: Self = Self {
|
||||||
|
value: constants::ZERO,
|
||||||
|
};
|
||||||
|
|
||||||
fn iterated_binary_vec(
|
fn iterated_binary_vec(
|
||||||
self,
|
self,
|
||||||
@ -269,7 +311,7 @@ impl fmt::Display for Number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Based on https://stackoverflow.com/a/65266882
|
// Based on https://stackoverflow.com/a/65266882
|
||||||
fn scientific(f: f64, precision: usize) -> String {
|
fn scientific(f: Decimal, precision: usize) -> String {
|
||||||
let mut ret = format!("{:.precision$E}", f, precision = precision);
|
let mut ret = format!("{:.precision$E}", f, precision = precision);
|
||||||
let exp = ret.split_off(ret.find('E').unwrap_or(0));
|
let exp = ret.split_off(ret.find('E').unwrap_or(0));
|
||||||
let (exp_sign, exp) = exp
|
let (exp_sign, exp) = exp
|
||||||
@ -280,7 +322,7 @@ fn scientific(f: f64, precision: usize) -> String {
|
|||||||
format!("{}{} E{}{:0>pad$}", sign, ret, exp_sign, exp, pad = 2)
|
format!("{}{} E{}{:0>pad$}", sign, ret, exp_sign, exp, pad = 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn engineering(f: f64, precision: usize) -> String {
|
fn engineering(f: Decimal, precision: usize) -> String {
|
||||||
// Format the string so the first digit is always in the first column, and remove '.'. Requested precision + 2 to account for using 1, 2, or 3 digits for the whole portion of the string
|
// Format the string so the first digit is always in the first column, and remove '.'. Requested precision + 2 to account for using 1, 2, or 3 digits for the whole portion of the string
|
||||||
// 1,000 => 1000E3
|
// 1,000 => 1000E3
|
||||||
let all = format!(" {:.precision$E}", f, precision = precision)
|
let all = format!(" {:.precision$E}", f, precision = precision)
|
||||||
@ -331,7 +373,7 @@ fn engineering(f: f64, precision: usize) -> String {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn separated(f: f64, sep: char) -> String {
|
fn separated(f: Decimal, sep: char) -> String {
|
||||||
let mut ret = f.to_string();
|
let mut ret = f.to_string();
|
||||||
let start = if ret.starts_with('-') { 1 } else { 0 };
|
let start = if ret.starts_with('-') { 1 } else { 0 };
|
||||||
let end = ret.find('.').unwrap_or_else(|| ret.len());
|
let end = ret.find('.').unwrap_or_else(|| ret.len());
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
use super::{Entry, Matrix, Number};
|
use super::{Entry, Matrix, Number};
|
||||||
use crate::{
|
use crate::{
|
||||||
calc::{
|
calc::{
|
||||||
|
entries::CalculatorEntry,
|
||||||
errors::{CalculatorError, CalculatorResult},
|
errors::{CalculatorError, CalculatorResult},
|
||||||
types::CalculatorAngleMode,
|
types::{CalculatorAngleMode, CalculatorDisplayMode},
|
||||||
CalculatorDisplayMode,
|
|
||||||
},
|
},
|
||||||
CalculatorEntry,
|
constants,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@ -77,11 +77,13 @@ impl CalculatorEntry for Vector {
|
|||||||
self.iterated_unary(Number::negate)
|
self.iterated_unary(Number::negate)
|
||||||
}
|
}
|
||||||
fn abs(&self) -> CalculatorResult<Entry> {
|
fn abs(&self) -> CalculatorResult<Entry> {
|
||||||
let value: Entry = self
|
let value: Entry =
|
||||||
.values
|
self.values
|
||||||
.iter()
|
.iter()
|
||||||
.try_fold(Entry::Number(Number::ZERO), |acc, n2| {
|
.try_fold(Entry::Number(Number::ZERO), |acc, n2| {
|
||||||
acc.add(&n2.pow(&Entry::Number(Number { value: 2.0_f64 }))?)
|
acc.add(&n2.pow(&Entry::Number(Number {
|
||||||
|
value: constants::TWO,
|
||||||
|
}))?)
|
||||||
})?;
|
})?;
|
||||||
value.sqrt()
|
value.sqrt()
|
||||||
}
|
}
|
||||||
|
26
src/constants.rs
Normal file
26
src/constants.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
use rust_decimal::Decimal;
|
||||||
|
use rust_decimal_macros::dec;
|
||||||
|
|
||||||
|
pub const ZERO: Decimal = Decimal::ZERO;
|
||||||
|
pub const ONE: Decimal = Decimal::ONE;
|
||||||
|
pub const TWO: Decimal = Decimal::TWO;
|
||||||
|
|
||||||
|
pub const PI: Decimal = Decimal::PI;
|
||||||
|
pub const E: Decimal = Decimal::E;
|
||||||
|
pub const TAU: Decimal = Decimal::TWO_PI;
|
||||||
|
|
||||||
|
pub const RAD_TO_DEC_MULTIPLIER: Decimal = dec!(57.295779513082320876798154814);
|
||||||
|
pub const DEC_TO_RAD_MULTIPLIER: Decimal = dec!(0.0174532925199432957692369077);
|
||||||
|
pub const GRAD_TO_RAD_MULTIPLIER: Decimal = dec!(0.0157079632679489661923132169);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_constants() {
|
||||||
|
assert_eq!(RAD_TO_DEC_MULTIPLIER, dec!(180.0) / PI);
|
||||||
|
assert_eq!(DEC_TO_RAD_MULTIPLIER, PI / dec!(180.0));
|
||||||
|
assert_eq!(GRAD_TO_RAD_MULTIPLIER, PI / dec!(200.0));
|
||||||
|
}
|
||||||
|
}
|
2
src/lib.rs
Normal file
2
src/lib.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod calc;
|
||||||
|
pub mod constants;
|
Loading…
Reference in New Issue
Block a user