Compare commits
No commits in common. "master" and "v0.4.0" have entirely different histories.
40
.drone.yml
40
.drone.yml
@ -1,40 +0,0 @@
|
|||||||
---
|
|
||||||
kind: pipeline
|
|
||||||
name: rpn_rs
|
|
||||||
type: docker
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Run earthly
|
|
||||||
image: earthly/earthly
|
|
||||||
privileged: true
|
|
||||||
volumes:
|
|
||||||
- name: docker_sock
|
|
||||||
path: /var/run/docker.sock
|
|
||||||
- name: dist
|
|
||||||
path: /dist
|
|
||||||
commands:
|
|
||||||
- 'file /var/run/docker.sock || :'
|
|
||||||
- 'pwd'
|
|
||||||
- 'earthly +all'
|
|
||||||
- 'cp -v ./dist/* /dist/'
|
|
||||||
|
|
||||||
- name: Gitea Artifacts
|
|
||||||
image: plugins/gitea-release
|
|
||||||
when:
|
|
||||||
event:
|
|
||||||
- tag
|
|
||||||
settings:
|
|
||||||
api_key:
|
|
||||||
from_secret: GITEA_API_KEY
|
|
||||||
base_url: https://gitea.austen-wares.com
|
|
||||||
files: dist/*
|
|
||||||
volumes:
|
|
||||||
- name: dist
|
|
||||||
path: /dist
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- name: docker_sock
|
|
||||||
host:
|
|
||||||
path: /var/run/docker.sock
|
|
||||||
- name: dist
|
|
||||||
temp: {}
|
|
@ -2,4 +2,3 @@
|
|||||||
!/src
|
!/src
|
||||||
!/Cargo.*
|
!/Cargo.*
|
||||||
!/.cargo
|
!/.cargo
|
||||||
!/README.adoc
|
|
||||||
|
3420
Cargo.lock
generated
3420
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
36
Cargo.toml
36
Cargo.toml
@ -1,42 +1,14 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rpn_rs"
|
name = "rpn_rs"
|
||||||
version = "0.6.0"
|
version = "0.4.0"
|
||||||
description = "A TUI RPN calculator, similar to Orpie"
|
|
||||||
authors = ["Austen Adler <agadler@austenadler.com>"]
|
authors = ["Austen Adler <agadler@austenadler.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
readme = "README.adoc"
|
readme = "README.adoc"
|
||||||
license = "GPL-3.0-only"
|
|
||||||
# TODO: Add the URL here
|
|
||||||
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"
|
|
||||||
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]
|
|
||||||
# TODO: Update this to v0.5.0 when it finally comes out -- for now, use latest git master
|
|
||||||
# version = "0.4.0"
|
|
||||||
git = "https://github.com/rust-cli/confy/"
|
|
||||||
# TOML will not serialize because of ordering issues
|
|
||||||
features = ["yaml_conf"]
|
|
||||||
default-features = false
|
|
||||||
|
|
||||||
[package.metadata.deb]
|
|
||||||
depends = ""
|
|
||||||
|
@ -58,7 +58,7 @@ asciidoctor REAMDE.adoc
|
|||||||
|
|
||||||
If you have not done this before, open the calculator and press `<ctrl+s>` to save the basic config.
|
If you have not done this before, open the calculator and press `<ctrl+s>` to save the basic config.
|
||||||
|
|
||||||
Edit the config file (in Linux, edit `~/.config/rpn_rs/rpn_rs.yaml` and add any constants or macros and press `<ctrl+l>` or reopen the calculator.
|
Edit the config file (in Linux, edit `~/.config/rpn_rs/rpn_rs.toml` and add any constants or macros and press `<ctrl+l>` or reopen the calculator.
|
||||||
|
|
||||||
Sample Macros:
|
Sample Macros:
|
||||||
|
|
||||||
@ -133,7 +133,3 @@ 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
|
|
||||||
|
10
build-mac.sh
10
build-mac.sh
@ -1,10 +0,0 @@
|
|||||||
#!/usr/bin/env zsh
|
|
||||||
set -euxo pipefail
|
|
||||||
TARGET=target/x86_64-apple-darwin/release/rpn_rs
|
|
||||||
cargo build --release --target x86_64-apple-darwin
|
|
||||||
du -sh "./${TARGET}"
|
|
||||||
strip "./${TARGET}"
|
|
||||||
du -sh "./${TARGET}"
|
|
||||||
gzip -f "./${TARGET}"
|
|
||||||
du -sh "./${TARGET}.gz"
|
|
||||||
mv "./${TARGET}.gz" "./target/release/rpn_rs-x86_64-apple-darwin.gz"
|
|
43
build.earth
43
build.earth
@ -1,6 +1,6 @@
|
|||||||
VERSION 0.6
|
|
||||||
FROM rust:latest
|
FROM rust:latest
|
||||||
ENV CARGO_HOME=/deps
|
ENV CARGO_HOME=/deps
|
||||||
|
ARG APP_NAME=rpn_rs
|
||||||
WORKDIR /work
|
WORKDIR /work
|
||||||
|
|
||||||
deps:
|
deps:
|
||||||
@ -11,61 +11,33 @@ deps:
|
|||||||
SAVE ARTIFACT /deps
|
SAVE ARTIFACT /deps
|
||||||
|
|
||||||
rust-builder:
|
rust-builder:
|
||||||
# Linux
|
|
||||||
RUN rustup target add x86_64-unknown-linux-musl
|
RUN rustup target add x86_64-unknown-linux-musl
|
||||||
RUN rustup target add i686-unknown-linux-musl
|
RUN rustup target add x86_64-pc-windows-gnu
|
||||||
RUN rustup target add i686-unknown-linux-musl
|
RUN rustup target add i686-unknown-linux-musl
|
||||||
RUN rustup target add aarch64-unknown-linux-musl
|
RUN rustup target add aarch64-unknown-linux-musl
|
||||||
RUN rustup target add arm-unknown-linux-musleabi
|
RUN rustup target add arm-unknown-linux-musleabi
|
||||||
RUN rustup target add armv7-unknown-linux-musleabi
|
RUN rustup target add armv7-unknown-linux-musleabi
|
||||||
# Windows
|
|
||||||
RUN rustup target add x86_64-pc-windows-gnu
|
|
||||||
RUN rustup target add i686-pc-windows-gnu
|
|
||||||
|
|
||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y gcc-arm-linux-gnueabi gcc-aarch64-linux-gnu gdb-mingw-w64 gcc-mingw-w64-x86-64 gcc-mingw-w64-i686
|
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y gcc-arm-linux-gnueabi gcc-aarch64-linux-gnu gdb-mingw-w64 gcc-mingw-w64-x86-64
|
||||||
|
|
||||||
RUN cargo install cargo-deb
|
|
||||||
|
|
||||||
SAVE IMAGE docker-wg:5000/rust-builder
|
SAVE IMAGE docker-wg:5000/rust-builder
|
||||||
|
|
||||||
build:
|
build:
|
||||||
FROM +rust-builder
|
FROM +rust-builder
|
||||||
|
COPY . .
|
||||||
COPY +deps/deps /deps
|
COPY +deps/deps /deps
|
||||||
|
|
||||||
ARG TOOLCHAIN
|
ARG TOOLCHAIN
|
||||||
|
|
||||||
# Cache whatever dependency builds you can
|
RUN cargo build --release --target "$TOOLCHAIN"
|
||||||
COPY ./Cargo.toml ./Cargo.lock .
|
|
||||||
RUN \
|
|
||||||
mkdir src && touch src/lib.rs && \
|
|
||||||
cargo build --offline --release --target "$TOOLCHAIN" `#--jobs 1` && \
|
|
||||||
rm src/lib.rs && rmdir src && \
|
|
||||||
:
|
|
||||||
|
|
||||||
# Actually build the program
|
|
||||||
COPY . .
|
|
||||||
RUN cargo build --offline --release --target "$TOOLCHAIN" `#--jobs 1`
|
|
||||||
|
|
||||||
ARG EXT
|
ARG EXT
|
||||||
ARG STRIP_CMD
|
ARG STRIP_CMD
|
||||||
ARG APP_NAME=rpn_rs
|
|
||||||
|
|
||||||
RUN if [ "$STRIP_CMD" ]; then "$STRIP_CMD" "target/$TOOLCHAIN/release/$APP_NAME$EXT"; fi
|
RUN if [ "$STRIP_CMD" ]; then "$STRIP_CMD" "target/$TOOLCHAIN/release/$APP_NAME$EXT"; fi
|
||||||
# --force for windows since multiple hardlinks
|
# --force for windows since multiple hardlinks
|
||||||
RUN xz --force --keep "target/$TOOLCHAIN/release/$APP_NAME$EXT"
|
RUN xz --force --keep "target/$TOOLCHAIN/release/$APP_NAME$EXT"
|
||||||
|
SAVE ARTIFACT target/$TOOLCHAIN/release/$APP_NAME$EXT AS LOCAL target/$TOOLCHAIN/release/$APP_NAME-$TOOLCHAIN$EXT
|
||||||
# Also build a deb file
|
SAVE ARTIFACT target/$TOOLCHAIN/release/$APP_NAME$EXT.xz AS LOCAL target/$TOOLCHAIN/release/$APP_NAME-$TOOLCHAIN$EXT.xz
|
||||||
IF [ -z "${TOOLCHAIN##*-linux-*}" ]
|
|
||||||
RUN echo "Building deb for ${TOOLCHAIN}"
|
|
||||||
RUN cargo deb --target "${TOOLCHAIN}"
|
|
||||||
|
|
||||||
SAVE ARTIFACT target/$TOOLCHAIN/debian/$APP_NAME_*.deb AS LOCAL dist/
|
|
||||||
END
|
|
||||||
|
|
||||||
# SAVE ARTIFACT target/$TOOLCHAIN/release/$APP_NAME$EXT AS LOCAL dist/
|
|
||||||
SAVE ARTIFACT target/$TOOLCHAIN/release/$APP_NAME$EXT.xz AS LOCAL dist/$APP_NAME-$TOOLCHAIN$EXT.xz
|
|
||||||
|
|
||||||
all:
|
all:
|
||||||
BUILD --build-arg TOOLCHAIN=x86_64-unknown-linux-musl --build-arg STRIP_CMD=x86_64-linux-gnu-strip +build
|
BUILD --build-arg TOOLCHAIN=x86_64-unknown-linux-musl --build-arg STRIP_CMD=x86_64-linux-gnu-strip +build
|
||||||
@ -74,4 +46,3 @@ all:
|
|||||||
BUILD --build-arg TOOLCHAIN=arm-unknown-linux-musleabi --build-arg STRIP_CMD=arm-linux-gnueabi-strip +build
|
BUILD --build-arg TOOLCHAIN=arm-unknown-linux-musleabi --build-arg STRIP_CMD=arm-linux-gnueabi-strip +build
|
||||||
BUILD --build-arg TOOLCHAIN=armv7-unknown-linux-musleabi --build-arg STRIP_CMD=arm-linux-gnueabi-strip +build
|
BUILD --build-arg TOOLCHAIN=armv7-unknown-linux-musleabi --build-arg STRIP_CMD=arm-linux-gnueabi-strip +build
|
||||||
BUILD --build-arg TOOLCHAIN=x86_64-pc-windows-gnu --build-arg STRIP_CMD= --build-arg EXT=.exe +build
|
BUILD --build-arg TOOLCHAIN=x86_64-pc-windows-gnu --build-arg STRIP_CMD= --build-arg EXT=.exe +build
|
||||||
BUILD --build-arg TOOLCHAIN=i686-pc-windows-gnu --build-arg STRIP_CMD= --build-arg EXT=.exe +build
|
|
||||||
|
22
pipeline.yml
22
pipeline.yml
@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
resources:
|
|
||||||
- name: source
|
|
||||||
type: git
|
|
||||||
source:
|
|
||||||
uri: https://gitea.austen-wares.com/stonewareslord/rpn_rs
|
|
||||||
branch: develop
|
|
||||||
jobs:
|
|
||||||
- name: build
|
|
||||||
serial: true
|
|
||||||
plan:
|
|
||||||
- get: source
|
|
||||||
- task: build
|
|
||||||
config:
|
|
||||||
platform: linux
|
|
||||||
image_resource:
|
|
||||||
type: docker-image
|
|
||||||
source: {repository: }
|
|
||||||
inputs:
|
|
||||||
- name: source
|
|
||||||
outputs:
|
|
||||||
- name: out
|
|
@ -1,6 +0,0 @@
|
|||||||
# 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
45
rpn_rs_gui/.github/workflows/pages.yml
vendored
@ -1,45 +0,0 @@
|
|||||||
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
105
rpn_rs_gui/.github/workflows/rust.yml
vendored
@ -1,105 +0,0 @@
|
|||||||
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
2
rpn_rs_gui/.gitignore
vendored
@ -1,2 +0,0 @@
|
|||||||
/target
|
|
||||||
/dist
|
|
2626
rpn_rs_gui/Cargo.lock
generated
2626
rpn_rs_gui/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,48 +0,0 @@
|
|||||||
[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" }
|
|
Binary file not shown.
@ -1,75 +0,0 @@
|
|||||||
# 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).
|
|
@ -1,2 +0,0 @@
|
|||||||
[build]
|
|
||||||
filehash = false
|
|
Binary file not shown.
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
Before Width: | Height: | Size: 314 KiB |
Binary file not shown.
Before Width: | Height: | Size: 47 KiB |
Binary file not shown.
Before Width: | Height: | Size: 21 KiB |
@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 128 KiB |
@ -1,30 +0,0 @@
|
|||||||
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);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,11 +0,0 @@
|
|||||||
#!/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
|
|
@ -1,140 +0,0 @@
|
|||||||
<!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/ -->
|
|
@ -1,539 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,108 +0,0 @@
|
|||||||
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"),
|
|
||||||
];
|
|
@ -1,4 +0,0 @@
|
|||||||
#![warn(clippy::all, rust_2018_idioms)]
|
|
||||||
|
|
||||||
mod app;
|
|
||||||
pub use app::TemplateApp;
|
|
@ -1,39 +0,0 @@
|
|||||||
#![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");
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
[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"
|
|
@ -1,2 +0,0 @@
|
|||||||
unstable_features = true
|
|
||||||
imports_granularity = "Crate"
|
|
763
src/calc.rs
763
src/calc.rs
File diff suppressed because it is too large
Load Diff
114
src/calc/constants.rs
Normal file
114
src/calc/constants.rs
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::hash_map::Iter;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum RegisterState {
|
||||||
|
Save,
|
||||||
|
Load,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum CalculatorState {
|
||||||
|
Normal,
|
||||||
|
WaitingForConstant,
|
||||||
|
WaitingForMacro,
|
||||||
|
WaitingForRegister(RegisterState),
|
||||||
|
WaitingForSetting,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CalculatorState {
|
||||||
|
fn default() -> Self {
|
||||||
|
CalculatorState::Normal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "constant")]
|
||||||
|
pub struct CalculatorConstant {
|
||||||
|
pub help: String,
|
||||||
|
pub value: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "macro")]
|
||||||
|
pub struct CalculatorMacro {
|
||||||
|
pub help: String,
|
||||||
|
pub value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type CalculatorConstants = HashMap<char, CalculatorConstant>;
|
||||||
|
pub type CalculatorConstantsIter<'a> = Iter<'a, char, CalculatorConstant>;
|
||||||
|
|
||||||
|
pub type CalculatorMacros = HashMap<char, CalculatorMacro>;
|
||||||
|
pub type CalculatorMacrosIter<'a> = Iter<'a, char, CalculatorMacro>;
|
||||||
|
|
||||||
|
pub type CalculatorRegisters = HashMap<char, f64>;
|
||||||
|
pub type CalculatorRegistersIter<'a> = Iter<'a, char, f64>;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "angle_mode")]
|
||||||
|
pub enum CalculatorAngleMode {
|
||||||
|
Degrees,
|
||||||
|
Radians,
|
||||||
|
Grads,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CalculatorAngleMode {
|
||||||
|
fn default() -> Self {
|
||||||
|
CalculatorAngleMode::Degrees
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for CalculatorAngleMode {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
CalculatorAngleMode::Degrees => write!(f, "DEG"),
|
||||||
|
CalculatorAngleMode::Radians => write!(f, "RAD"),
|
||||||
|
CalculatorAngleMode::Grads => write!(f, "GRD"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "display_mode")]
|
||||||
|
// Could also have added content="precision"
|
||||||
|
pub enum CalculatorDisplayMode {
|
||||||
|
Default,
|
||||||
|
Separated { separator: char },
|
||||||
|
Scientific { precision: usize },
|
||||||
|
Engineering { precision: usize },
|
||||||
|
Fixed { precision: usize },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for CalculatorDisplayMode {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
CalculatorDisplayMode::Default => write!(f, "DEF"),
|
||||||
|
CalculatorDisplayMode::Separated { separator } => write!(f, "SEP({})", separator),
|
||||||
|
CalculatorDisplayMode::Scientific { precision } => write!(f, "SCI({})", precision),
|
||||||
|
CalculatorDisplayMode::Engineering { precision } => write!(f, "ENG({})", precision),
|
||||||
|
CalculatorDisplayMode::Fixed { precision } => write!(f, "FIX({})", precision),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CalculatorDisplayMode {
|
||||||
|
fn default() -> Self {
|
||||||
|
CalculatorDisplayMode::Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
// #[serde(tag = "calculator_alignment")]
|
||||||
|
// pub enum CalculatorAlignment {
|
||||||
|
// Right,
|
||||||
|
// Left,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl Default for CalculatorAlignment {
|
||||||
|
// fn default() -> Self {
|
||||||
|
// CalculatorAlignment::Left
|
||||||
|
// }
|
||||||
|
// }
|
@ -1,496 +0,0 @@
|
|||||||
// TODO: Clippy is recommending pass by value instead of by ref, but I plan to add imaginary numbers, which will change this
|
|
||||||
#![allow(clippy::trivially_copy_pass_by_ref)]
|
|
||||||
mod matrix;
|
|
||||||
mod number;
|
|
||||||
mod vector;
|
|
||||||
use super::errors::CalculatorResult;
|
|
||||||
use crate::calc::{types::CalculatorAngleMode, CalculatorDisplayMode};
|
|
||||||
pub use matrix::{Matrix, MatrixDimensions};
|
|
||||||
pub use number::Number;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::{cmp::PartialEq, fmt};
|
|
||||||
pub use vector::{Vector, VectorDirection};
|
|
||||||
|
|
||||||
pub trait CalculatorEntry
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
// Misc
|
|
||||||
fn is_valid(&self) -> bool;
|
|
||||||
/// Turns self into a valid entry
|
|
||||||
fn validate(self) -> CalculatorResult<Entry>;
|
|
||||||
fn format_entry(&self, display_mode: &CalculatorDisplayMode) -> String;
|
|
||||||
fn to_editable_string(&self) -> CalculatorResult<String>;
|
|
||||||
// Mathematical operations
|
|
||||||
// Unary
|
|
||||||
fn negate(&self) -> CalculatorResult<Entry>;
|
|
||||||
fn abs(&self) -> CalculatorResult<Entry>;
|
|
||||||
fn inverse(&self) -> CalculatorResult<Entry>;
|
|
||||||
fn transpose(&self) -> CalculatorResult<Entry>;
|
|
||||||
fn sin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry>;
|
|
||||||
fn cos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry>;
|
|
||||||
fn tan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry>;
|
|
||||||
fn asin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry>;
|
|
||||||
fn acos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry>;
|
|
||||||
fn atan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry>;
|
|
||||||
fn sqrt(&self) -> CalculatorResult<Entry>;
|
|
||||||
fn log(&self) -> CalculatorResult<Entry>;
|
|
||||||
fn ln(&self) -> CalculatorResult<Entry>;
|
|
||||||
|
|
||||||
// Binary
|
|
||||||
fn add(&self, arg: &Entry) -> CalculatorResult<Entry>;
|
|
||||||
fn sub(&self, arg: &Entry) -> CalculatorResult<Entry>;
|
|
||||||
fn mul(&self, arg: &Entry) -> CalculatorResult<Entry>;
|
|
||||||
fn div(&self, arg: &Entry) -> CalculatorResult<Entry>;
|
|
||||||
fn int_divide(&self, arg: &Entry) -> CalculatorResult<Entry>;
|
|
||||||
fn modulo(&self, arg: &Entry) -> CalculatorResult<Entry>;
|
|
||||||
fn pow(&self, arg: &Entry) -> CalculatorResult<Entry>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
pub enum Entry {
|
|
||||||
Number(Number),
|
|
||||||
Vector(Vector),
|
|
||||||
Matrix(Matrix),
|
|
||||||
}
|
|
||||||
|
|
||||||
// macro_rules! op_child_call {
|
|
||||||
// ($a:ident, $op:ident) => {
|
|
||||||
// match $a {
|
|
||||||
// Self::Number(number) => number.$op(),
|
|
||||||
// Self::Vector(vector) => vector.$op(),
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
// ($a:ident, $op:ident, $arg:expr) => {
|
|
||||||
// match $a {
|
|
||||||
// Self::Number(number) => number.$op($arg),
|
|
||||||
// Self::Vector(vector) => vector.$op($arg),
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
impl CalculatorEntry for Entry {
|
|
||||||
fn to_editable_string(&self) -> CalculatorResult<String> {
|
|
||||||
match self {
|
|
||||||
Self::Number(number) => number.to_editable_string(),
|
|
||||||
Self::Vector(vector) => vector.to_editable_string(),
|
|
||||||
Self::Matrix(matrix) => matrix.to_editable_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn format_entry(&self, display_mode: &CalculatorDisplayMode) -> String {
|
|
||||||
match self {
|
|
||||||
Self::Number(number) => number.format_entry(display_mode),
|
|
||||||
Self::Vector(vector) => vector.format_entry(display_mode),
|
|
||||||
Self::Matrix(matrix) => matrix.format_entry(display_mode),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn is_valid(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
Self::Number(number) => number.is_valid(),
|
|
||||||
Self::Vector(vector) => vector.is_valid(),
|
|
||||||
Self::Matrix(matrix) => matrix.is_valid(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn validate(self) -> CalculatorResult<Entry> {
|
|
||||||
match self {
|
|
||||||
Self::Number(number) => number.validate(),
|
|
||||||
Self::Vector(vector) => vector.validate(),
|
|
||||||
Self::Matrix(matrix) => matrix.validate(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn negate(&self) -> CalculatorResult<Self> {
|
|
||||||
match self {
|
|
||||||
Self::Number(number) => number.negate(),
|
|
||||||
Self::Vector(vector) => vector.negate(),
|
|
||||||
Self::Matrix(matrix) => matrix.negate(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn abs(&self) -> CalculatorResult<Self> {
|
|
||||||
match self {
|
|
||||||
Self::Number(number) => number.abs(),
|
|
||||||
Self::Vector(vector) => vector.abs(),
|
|
||||||
Self::Matrix(matrix) => matrix.abs(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn inverse(&self) -> CalculatorResult<Self> {
|
|
||||||
match self {
|
|
||||||
Self::Number(number) => number.inverse(),
|
|
||||||
Self::Vector(vector) => vector.inverse(),
|
|
||||||
Self::Matrix(matrix) => matrix.inverse(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn transpose(&self) -> CalculatorResult<Self> {
|
|
||||||
match self {
|
|
||||||
Self::Number(number) => number.transpose(),
|
|
||||||
Self::Vector(vector) => vector.transpose(),
|
|
||||||
Self::Matrix(matrix) => matrix.transpose(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn sin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Self> {
|
|
||||||
match self {
|
|
||||||
Self::Number(number) => number.sin(angle_mode),
|
|
||||||
Self::Vector(vector) => vector.sin(angle_mode),
|
|
||||||
Self::Matrix(matrix) => matrix.sin(angle_mode),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn cos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Self> {
|
|
||||||
match self {
|
|
||||||
Self::Number(number) => number.cos(angle_mode),
|
|
||||||
Self::Vector(vector) => vector.cos(angle_mode),
|
|
||||||
Self::Matrix(matrix) => matrix.cos(angle_mode),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn tan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Self> {
|
|
||||||
match self {
|
|
||||||
Self::Number(number) => number.tan(angle_mode),
|
|
||||||
Self::Vector(vector) => vector.tan(angle_mode),
|
|
||||||
Self::Matrix(matrix) => matrix.tan(angle_mode),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn asin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Self> {
|
|
||||||
match self {
|
|
||||||
Self::Number(number) => number.asin(angle_mode),
|
|
||||||
Self::Vector(vector) => vector.asin(angle_mode),
|
|
||||||
Self::Matrix(matrix) => matrix.asin(angle_mode),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn acos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Self> {
|
|
||||||
match self {
|
|
||||||
Self::Number(number) => number.acos(angle_mode),
|
|
||||||
Self::Vector(vector) => vector.acos(angle_mode),
|
|
||||||
Self::Matrix(matrix) => matrix.acos(angle_mode),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn atan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Self> {
|
|
||||||
match self {
|
|
||||||
Self::Number(number) => number.atan(angle_mode),
|
|
||||||
Self::Vector(vector) => vector.atan(angle_mode),
|
|
||||||
Self::Matrix(matrix) => matrix.atan(angle_mode),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn sqrt(&self) -> CalculatorResult<Self> {
|
|
||||||
match self {
|
|
||||||
Self::Number(number) => number.sqrt(),
|
|
||||||
Self::Vector(vector) => vector.sqrt(),
|
|
||||||
Self::Matrix(matrix) => matrix.sqrt(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn log(&self) -> CalculatorResult<Self> {
|
|
||||||
match self {
|
|
||||||
Self::Number(number) => number.log(),
|
|
||||||
Self::Vector(vector) => vector.log(),
|
|
||||||
Self::Matrix(matrix) => matrix.log(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn ln(&self) -> CalculatorResult<Self> {
|
|
||||||
match self {
|
|
||||||
Self::Number(number) => number.ln(),
|
|
||||||
Self::Vector(vector) => vector.ln(),
|
|
||||||
Self::Matrix(matrix) => matrix.ln(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add(&self, arg: &Self) -> CalculatorResult<Self> {
|
|
||||||
match self {
|
|
||||||
Self::Number(number) => number.add(arg),
|
|
||||||
Self::Vector(vector) => vector.add(arg),
|
|
||||||
Self::Matrix(matrix) => matrix.add(arg),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn sub(&self, arg: &Self) -> CalculatorResult<Self> {
|
|
||||||
match self {
|
|
||||||
Self::Number(number) => number.sub(arg),
|
|
||||||
Self::Vector(vector) => vector.sub(arg),
|
|
||||||
Self::Matrix(matrix) => matrix.sub(arg),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn mul(&self, arg: &Self) -> CalculatorResult<Self> {
|
|
||||||
match self {
|
|
||||||
Self::Number(number) => number.mul(arg),
|
|
||||||
Self::Vector(vector) => vector.mul(arg),
|
|
||||||
Self::Matrix(matrix) => matrix.mul(arg),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn div(&self, arg: &Self) -> CalculatorResult<Self> {
|
|
||||||
match self {
|
|
||||||
Self::Number(number) => number.div(arg),
|
|
||||||
Self::Vector(vector) => vector.div(arg),
|
|
||||||
Self::Matrix(matrix) => matrix.div(arg),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn int_divide(&self, arg: &Self) -> CalculatorResult<Self> {
|
|
||||||
match self {
|
|
||||||
Self::Number(number) => number.int_divide(arg),
|
|
||||||
Self::Vector(vector) => vector.int_divide(arg),
|
|
||||||
Self::Matrix(matrix) => matrix.int_divide(arg),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn modulo(&self, arg: &Self) -> CalculatorResult<Self> {
|
|
||||||
match self {
|
|
||||||
Self::Number(number) => number.modulo(arg),
|
|
||||||
Self::Vector(vector) => vector.modulo(arg),
|
|
||||||
Self::Matrix(matrix) => matrix.modulo(arg),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn pow(&self, arg: &Self) -> CalculatorResult<Self> {
|
|
||||||
match self {
|
|
||||||
Self::Number(number) => number.pow(arg),
|
|
||||||
Self::Vector(vector) => vector.pow(arg),
|
|
||||||
Self::Matrix(matrix) => matrix.pow(arg),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Entry {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Matrix(matrix) => write!(f, "{}", matrix),
|
|
||||||
Self::Vector(vector) => write!(f, "{}", vector),
|
|
||||||
Self::Number(number) => write!(f, "{}", number),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use rust_decimal::Decimal;
|
|
||||||
use rust_decimal_macros::dec;
|
|
||||||
|
|
||||||
fn valid_square_matrix() -> Entry {
|
|
||||||
Entry::Matrix(Matrix {
|
|
||||||
dimensions: MatrixDimensions {
|
|
||||||
columns: 3,
|
|
||||||
rows: 3,
|
|
||||||
},
|
|
||||||
vectors: vec![
|
|
||||||
Vector {
|
|
||||||
direction: VectorDirection::Column,
|
|
||||||
values: vec![
|
|
||||||
Number { value: dec!(1.0) },
|
|
||||||
Number { value: dec!(2.0) },
|
|
||||||
Number { value: dec!(-3.0) },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
direction: VectorDirection::Column,
|
|
||||||
values: vec![
|
|
||||||
Number { value: dec!(4.0) },
|
|
||||||
Number { value: dec!(-5.0) },
|
|
||||||
Number { value: dec!(0.0) },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
Vector {
|
|
||||||
direction: VectorDirection::Column,
|
|
||||||
values: vec![
|
|
||||||
Number { value: dec!(-7.0) },
|
|
||||||
Number { value: dec!(8.0) },
|
|
||||||
Number { value: dec!(9.0) },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn valid_cascading_operations() {
|
|
||||||
for (op, entry, goal) in &[
|
|
||||||
(
|
|
||||||
"negate",
|
|
||||||
valid_square_matrix().negate(),
|
|
||||||
Matrix::from(&[
|
|
||||||
Entry::Vector(Vector {
|
|
||||||
direction: VectorDirection::Column,
|
|
||||||
values: vec![
|
|
||||||
Number { value: dec!(-1.0) },
|
|
||||||
Number { value: dec!(-2.0) },
|
|
||||||
Number { value: dec!(3.0) },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
Entry::Vector(Vector {
|
|
||||||
direction: VectorDirection::Column,
|
|
||||||
values: vec![
|
|
||||||
Number { value: dec!(-4.0) },
|
|
||||||
Number { value: dec!(5.0) },
|
|
||||||
Number { value: dec!(-0.0) },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
Entry::Vector(Vector {
|
|
||||||
direction: VectorDirection::Column,
|
|
||||||
values: vec![
|
|
||||||
Number { value: dec!(7.0) },
|
|
||||||
Number { value: dec!(-8.0) },
|
|
||||||
Number { value: dec!(-9.0) },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
// (
|
|
||||||
// "abs",
|
|
||||||
// valid_square_matrix().abs(),
|
|
||||||
// Matrix::from(&[
|
|
||||||
// Entry::Vector(Vector {
|
|
||||||
// direction: VectorDirection::Column,
|
|
||||||
// values: vec![
|
|
||||||
// Number { value: dec!(1.0) },
|
|
||||||
// Number { value: dec!(2.0) },
|
|
||||||
// Number { value: dec!(3.0) },
|
|
||||||
// ],
|
|
||||||
// }),
|
|
||||||
// Entry::Vector(Vector {
|
|
||||||
// direction: VectorDirection::Column,
|
|
||||||
// values: vec![
|
|
||||||
// Number { value: dec!(4.0) },
|
|
||||||
// Number { value: dec!(5.0) },
|
|
||||||
// Number { value: dec!(0.0) },
|
|
||||||
// ],
|
|
||||||
// }),
|
|
||||||
// Entry::Vector(Vector {
|
|
||||||
// direction: VectorDirection::Column,
|
|
||||||
// values: vec![
|
|
||||||
// Number { value: dec!(7.0) },
|
|
||||||
// Number { value: dec!(8.0) },
|
|
||||||
// Number { value: dec!(9.0) },
|
|
||||||
// ],
|
|
||||||
// }),
|
|
||||||
// ]),
|
|
||||||
// ),
|
|
||||||
// (valid_square_matrix().inverse(), Matrix::from(
|
|
||||||
// Entry::Vector(Vector {
|
|
||||||
// direction: VectorDirection::Column,
|
|
||||||
// values: vec![
|
|
||||||
// Number{value: dec!(1.0},)
|
|
||||||
// Number{value: dec!(2.0},)
|
|
||||||
// Number{value: dec!(-3.0},)
|
|
||||||
// ]}),
|
|
||||||
// Entry::Vector (Vector{
|
|
||||||
// direction: VectorDirection::Column,
|
|
||||||
// values: vec![
|
|
||||||
// Number{value: dec!(4.0},)
|
|
||||||
// Number{value: dec!(-5.0},)
|
|
||||||
// Number{value: dec!(0.0},)
|
|
||||||
// ]}),
|
|
||||||
// Entry::Vector (Vector{
|
|
||||||
// direction: VectorDirection::Column,
|
|
||||||
// values: vec![
|
|
||||||
// Number{value: dec!(-7.0},)
|
|
||||||
// Number{value: dec!(8.0},)
|
|
||||||
// Number{value: dec!(9.0},)
|
|
||||||
// ])),
|
|
||||||
(
|
|
||||||
"transpose",
|
|
||||||
valid_square_matrix().transpose(),
|
|
||||||
Matrix::from(&[
|
|
||||||
Entry::Vector(Vector {
|
|
||||||
direction: VectorDirection::Column,
|
|
||||||
values: vec![
|
|
||||||
Number { value: dec!(1.0) },
|
|
||||||
Number { value: dec!(4.0) },
|
|
||||||
Number { value: dec!(-7.0) },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
Entry::Vector(Vector {
|
|
||||||
direction: VectorDirection::Column,
|
|
||||||
values: vec![
|
|
||||||
Number { value: dec!(2.0) },
|
|
||||||
Number { value: dec!(-5.0) },
|
|
||||||
Number { value: dec!(8.0) },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
Entry::Vector(Vector {
|
|
||||||
direction: VectorDirection::Column,
|
|
||||||
values: vec![
|
|
||||||
Number { value: dec!(-3.0) },
|
|
||||||
Number { value: dec!(0.0) },
|
|
||||||
Number { value: dec!(9.0) },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"sqrt",
|
|
||||||
(Matrix {
|
|
||||||
dimensions: MatrixDimensions {
|
|
||||||
columns: 1,
|
|
||||||
rows: 3,
|
|
||||||
},
|
|
||||||
vectors: vec![Vector {
|
|
||||||
direction: VectorDirection::Column,
|
|
||||||
values: vec![
|
|
||||||
Number { value: dec!(1.0) },
|
|
||||||
Number { value: dec!(100.0) },
|
|
||||||
Number { value: dec!(64.0) },
|
|
||||||
],
|
|
||||||
}],
|
|
||||||
})
|
|
||||||
.sqrt(),
|
|
||||||
Matrix::from(&[Entry::Vector(Vector {
|
|
||||||
direction: VectorDirection::Column,
|
|
||||||
values: vec![
|
|
||||||
Number { value: dec!(1.0) },
|
|
||||||
Number { value: dec!(10.0) },
|
|
||||||
Number { value: dec!(8.0) },
|
|
||||||
],
|
|
||||||
})]),
|
|
||||||
),
|
|
||||||
// NEWS
|
|
||||||
(
|
|
||||||
"log",
|
|
||||||
(Matrix {
|
|
||||||
dimensions: MatrixDimensions {
|
|
||||||
columns: 1,
|
|
||||||
rows: 3,
|
|
||||||
},
|
|
||||||
vectors: vec![Vector {
|
|
||||||
direction: VectorDirection::Column,
|
|
||||||
values: vec![
|
|
||||||
Number { value: dec!(1.0) },
|
|
||||||
Number { value: dec!(100.0) },
|
|
||||||
Number {
|
|
||||||
value: dec!(100_000.0),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}],
|
|
||||||
})
|
|
||||||
.log(),
|
|
||||||
Matrix::from(&[Entry::Vector(Vector {
|
|
||||||
direction: VectorDirection::Column,
|
|
||||||
values: vec![
|
|
||||||
Number { value: dec!(0.0) },
|
|
||||||
Number { value: dec!(2.0) },
|
|
||||||
Number { value: dec!(5.0) },
|
|
||||||
],
|
|
||||||
})]),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"ln",
|
|
||||||
(Matrix {
|
|
||||||
dimensions: MatrixDimensions {
|
|
||||||
columns: 1,
|
|
||||||
rows: 3,
|
|
||||||
},
|
|
||||||
vectors: vec![Vector {
|
|
||||||
direction: VectorDirection::Column,
|
|
||||||
values: vec![
|
|
||||||
Number { value: dec!(1.0) },
|
|
||||||
Number {
|
|
||||||
value: constants::E,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}],
|
|
||||||
})
|
|
||||||
.ln(),
|
|
||||||
Matrix::from(&[Entry::Vector(Vector {
|
|
||||||
direction: VectorDirection::Column,
|
|
||||||
values: vec![Number { value: dec!(0.0) }, Number { value: dec!(1.0) }],
|
|
||||||
})]),
|
|
||||||
),
|
|
||||||
] {
|
|
||||||
println!("Testing: {}", op);
|
|
||||||
let left = entry.as_ref().expect("Operation failed");
|
|
||||||
let right = goal.as_ref().expect("Invalid test goal construction");
|
|
||||||
assert_eq!(left, right);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,352 +0,0 @@
|
|||||||
use super::{Entry, Number, Vector, VectorDirection};
|
|
||||||
use crate::calc::{
|
|
||||||
entries::CalculatorEntry,
|
|
||||||
errors::{CalculatorError, CalculatorResult},
|
|
||||||
types::{CalculatorAngleMode, CalculatorDisplayMode},
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct MatrixDimensions {
|
|
||||||
pub rows: usize,
|
|
||||||
pub columns: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MatrixDimensions {
|
|
||||||
pub const fn transpose(&self) -> Self {
|
|
||||||
Self {
|
|
||||||
rows: self.columns,
|
|
||||||
columns: self.rows,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Matrix {
|
|
||||||
pub vectors: Vec<Vector>,
|
|
||||||
pub dimensions: MatrixDimensions,
|
|
||||||
}
|
|
||||||
impl Matrix {
|
|
||||||
pub fn from(entries: &[Entry]) -> CalculatorResult<Entry> {
|
|
||||||
if entries.is_empty() {
|
|
||||||
return Err(CalculatorError::NotEnoughStackEntries);
|
|
||||||
}
|
|
||||||
|
|
||||||
let vectors = entries
|
|
||||||
.iter()
|
|
||||||
.map(|e| match e {
|
|
||||||
Entry::Matrix(_) | Entry::Number(_) => Err(CalculatorError::TypeMismatch),
|
|
||||||
Entry::Vector(vector) => Ok(vector.clone()),
|
|
||||||
})
|
|
||||||
.collect::<CalculatorResult<Vec<Vector>>>()?;
|
|
||||||
|
|
||||||
// Get the num_rows and dimension of the matrix
|
|
||||||
let first_vector = vectors
|
|
||||||
.get(0)
|
|
||||||
.ok_or(CalculatorError::NotEnoughStackEntries)?;
|
|
||||||
|
|
||||||
// The number of rows in this column-based matrix
|
|
||||||
let num_rows = first_vector.values.len();
|
|
||||||
// The direction all vectors must face
|
|
||||||
let vector_direction = first_vector.direction;
|
|
||||||
|
|
||||||
// Either the dimension lengths mismatch, or the vectors are facing different directions (and are longer than 1, since a 1-length vector orientation does not matter
|
|
||||||
if vectors.iter().any(|v| v.values.len() != num_rows)
|
|
||||||
|| (num_rows > 1 && vectors.iter().any(|v| v.direction != vector_direction))
|
|
||||||
{
|
|
||||||
return Err(CalculatorError::DimensionMismatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
let dimensions = MatrixDimensions {
|
|
||||||
rows: num_rows,
|
|
||||||
columns: vectors.len(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let ret = Self {
|
|
||||||
vectors,
|
|
||||||
dimensions,
|
|
||||||
};
|
|
||||||
|
|
||||||
// If the user tried making a matrix out of row vectors, we need to transpose it, which forces column vectors
|
|
||||||
if vector_direction == VectorDirection::Row && num_rows > 1 {
|
|
||||||
ret.transpose()
|
|
||||||
} else {
|
|
||||||
ret.validate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn iterated_unary(
|
|
||||||
&self,
|
|
||||||
op: impl Fn(&Vector) -> CalculatorResult<Entry>,
|
|
||||||
) -> CalculatorResult<Entry> {
|
|
||||||
Self {
|
|
||||||
vectors: self
|
|
||||||
.vectors
|
|
||||||
.iter()
|
|
||||||
.map(|v| op(v))
|
|
||||||
.map(|e| match e {
|
|
||||||
Ok(Entry::Vector(vector)) => Ok(vector),
|
|
||||||
_ => Err(CalculatorError::ArithmeticError),
|
|
||||||
})
|
|
||||||
.collect::<CalculatorResult<Vec<Vector>>>()?,
|
|
||||||
dimensions: self.dimensions.clone(),
|
|
||||||
}
|
|
||||||
.validate()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn iterated_binary_num(
|
|
||||||
&self,
|
|
||||||
number: &Number,
|
|
||||||
op: impl Fn(&Vector, &Entry) -> CalculatorResult<Entry>,
|
|
||||||
) -> CalculatorResult<Entry> {
|
|
||||||
Self {
|
|
||||||
vectors: self
|
|
||||||
.vectors
|
|
||||||
.iter()
|
|
||||||
.map(|v| op(v, &Entry::Number(*number)))
|
|
||||||
.map(|e| match e {
|
|
||||||
Ok(Entry::Vector(vector)) => Ok(vector),
|
|
||||||
_ => Err(CalculatorError::ArithmeticError),
|
|
||||||
})
|
|
||||||
.collect::<CalculatorResult<Vec<Vector>>>()?,
|
|
||||||
dimensions: self.dimensions.clone(),
|
|
||||||
}
|
|
||||||
.validate()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn iterated_binary_mat(
|
|
||||||
&self,
|
|
||||||
m2: &Self,
|
|
||||||
op: impl Fn(&Vector, &Entry) -> CalculatorResult<Entry>,
|
|
||||||
) -> CalculatorResult<Entry> {
|
|
||||||
if self.dimensions != m2.dimensions {
|
|
||||||
return Err(CalculatorError::DimensionMismatch);
|
|
||||||
}
|
|
||||||
Self {
|
|
||||||
vectors: self
|
|
||||||
.vectors
|
|
||||||
.iter()
|
|
||||||
.zip(m2.vectors.iter())
|
|
||||||
.map(|(v1, v2)| op(v1, &Entry::Vector(v2.clone())))
|
|
||||||
.map(|e| match e {
|
|
||||||
Ok(Entry::Vector(vector)) => Ok(vector),
|
|
||||||
_ => Err(CalculatorError::ArithmeticError),
|
|
||||||
})
|
|
||||||
.collect::<CalculatorResult<Vec<Vector>>>()?,
|
|
||||||
dimensions: self.dimensions.clone(),
|
|
||||||
}
|
|
||||||
.validate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CalculatorEntry for Matrix {
|
|
||||||
fn to_editable_string(&self) -> CalculatorResult<String> {
|
|
||||||
Err(CalculatorError::TypeMismatch)
|
|
||||||
}
|
|
||||||
fn is_valid(&self) -> bool {
|
|
||||||
// The the number of vectors is equal to the 0th dimension
|
|
||||||
self.vectors.len() == self.dimensions.columns
|
|
||||||
// The number of elements in all vectors are equal to the 1st dimension, and each is valid
|
|
||||||
&& self
|
|
||||||
.vectors
|
|
||||||
.iter()
|
|
||||||
.all(|v| v.values.len() == self.dimensions.rows && v.is_valid())
|
|
||||||
// The dimensions are not zero
|
|
||||||
&& self.dimensions.rows > 0 && self.dimensions.columns > 0
|
|
||||||
}
|
|
||||||
fn validate(self) -> CalculatorResult<Entry> {
|
|
||||||
if self.is_valid() {
|
|
||||||
Ok(Entry::Matrix(self))
|
|
||||||
} else {
|
|
||||||
Err(CalculatorError::ArithmeticError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_entry(&self, display_mode: &CalculatorDisplayMode) -> String {
|
|
||||||
format!(
|
|
||||||
"[ {} ]",
|
|
||||||
self.vectors
|
|
||||||
.iter()
|
|
||||||
.map(|vector| vector.format_entry(display_mode))
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(" ")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mathematical operations
|
|
||||||
fn negate(&self) -> CalculatorResult<Entry> {
|
|
||||||
self.iterated_unary(Vector::negate)
|
|
||||||
}
|
|
||||||
fn abs(&self) -> CalculatorResult<Entry> {
|
|
||||||
// TODO: Compute determinant
|
|
||||||
Err(CalculatorError::NotYetImplemented)
|
|
||||||
}
|
|
||||||
fn inverse(&self) -> CalculatorResult<Entry> {
|
|
||||||
// TODO: Inverse
|
|
||||||
Err(CalculatorError::NotYetImplemented)
|
|
||||||
}
|
|
||||||
fn transpose(&self) -> CalculatorResult<Entry> {
|
|
||||||
// Iterate over all rows
|
|
||||||
let mut vectors: Vec<Vector> = vec![];
|
|
||||||
for r in 0..self.dimensions.rows {
|
|
||||||
vectors.push(Vector {
|
|
||||||
values: self
|
|
||||||
.vectors
|
|
||||||
.iter()
|
|
||||||
.map(|v| {
|
|
||||||
// For each row, get the r'th element to build a new vector
|
|
||||||
v.values
|
|
||||||
.get(r)
|
|
||||||
.map_or_else(|| Err(CalculatorError::DimensionMismatch), |n| Ok(*n))
|
|
||||||
})
|
|
||||||
.collect::<CalculatorResult<Vec<Number>>>()?,
|
|
||||||
direction: VectorDirection::Column,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
|
||||||
vectors,
|
|
||||||
dimensions: self.dimensions.transpose(),
|
|
||||||
}
|
|
||||||
.validate()
|
|
||||||
}
|
|
||||||
fn sin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
|
|
||||||
self.iterated_unary(|v| v.sin(angle_mode))
|
|
||||||
}
|
|
||||||
fn cos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
|
|
||||||
self.iterated_unary(|v| v.cos(angle_mode))
|
|
||||||
}
|
|
||||||
fn tan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
|
|
||||||
self.iterated_unary(|v| v.tan(angle_mode))
|
|
||||||
}
|
|
||||||
fn asin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
|
|
||||||
self.iterated_unary(|v| v.asin(angle_mode))
|
|
||||||
}
|
|
||||||
fn acos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
|
|
||||||
self.iterated_unary(|v| v.acos(angle_mode))
|
|
||||||
}
|
|
||||||
fn atan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
|
|
||||||
self.iterated_unary(|v| v.atan(angle_mode))
|
|
||||||
}
|
|
||||||
fn sqrt(&self) -> CalculatorResult<Entry> {
|
|
||||||
self.iterated_unary(Vector::sqrt)
|
|
||||||
}
|
|
||||||
fn log(&self) -> CalculatorResult<Entry> {
|
|
||||||
self.iterated_unary(Vector::log)
|
|
||||||
}
|
|
||||||
fn ln(&self) -> CalculatorResult<Entry> {
|
|
||||||
self.iterated_unary(Vector::ln)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Binary
|
|
||||||
fn add(&self, arg: &Entry) -> CalculatorResult<Entry> {
|
|
||||||
match arg {
|
|
||||||
Entry::Matrix(m2) => self.iterated_binary_mat(m2, Vector::add),
|
|
||||||
Entry::Vector(_vector) => Err(CalculatorError::TypeMismatch),
|
|
||||||
Entry::Number(number) => self.iterated_binary_num(number, Vector::add),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn sub(&self, arg: &Entry) -> CalculatorResult<Entry> {
|
|
||||||
match arg {
|
|
||||||
Entry::Matrix(m2) => self.iterated_binary_mat(m2, Vector::sub),
|
|
||||||
Entry::Vector(_vector) => Err(CalculatorError::TypeMismatch),
|
|
||||||
Entry::Number(number) => self.iterated_binary_num(number, Vector::sub),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn mul(&self, arg: &Entry) -> CalculatorResult<Entry> {
|
|
||||||
match arg {
|
|
||||||
Entry::Matrix(m2) => {
|
|
||||||
if self.dimensions.columns != m2.dimensions.rows {
|
|
||||||
return Err(CalculatorError::DimensionMismatch);
|
|
||||||
}
|
|
||||||
let dimensions = MatrixDimensions {
|
|
||||||
rows: self.dimensions.rows,
|
|
||||||
columns: m2.dimensions.columns,
|
|
||||||
};
|
|
||||||
|
|
||||||
// A matrix is a list of column vectors, so transpose self and zip the columns
|
|
||||||
let transposed_self: Self = match self.transpose()? {
|
|
||||||
Entry::Matrix(t) => t,
|
|
||||||
_ => {
|
|
||||||
return Err(CalculatorError::InternalError(String::from(
|
|
||||||
"Matrix transpose produced wrong type",
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut vectors: Vec<Vector> = vec![];
|
|
||||||
|
|
||||||
for c in &m2.vectors {
|
|
||||||
let mut vector: Vector = Vector {
|
|
||||||
values: vec![],
|
|
||||||
direction: VectorDirection::Column,
|
|
||||||
};
|
|
||||||
for r in &transposed_self.vectors {
|
|
||||||
if let Entry::Number(number) =
|
|
||||||
c.transpose()?.mul(&Entry::Vector(r.clone()))?
|
|
||||||
{
|
|
||||||
vector.values.push(number);
|
|
||||||
} else {
|
|
||||||
return Err(CalculatorError::InternalError(String::from(
|
|
||||||
"Vector multiplication did not produce a number",
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
vectors.push(vector);
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
|
||||||
vectors,
|
|
||||||
dimensions,
|
|
||||||
}
|
|
||||||
.validate()
|
|
||||||
}
|
|
||||||
Entry::Vector(vector) => self.mul(&Self::from(
|
|
||||||
&[Entry::Vector(vector.clone())], // Treat a vector as a 1D matrix
|
|
||||||
)?),
|
|
||||||
Entry::Number(number) => self.iterated_binary_num(number, Vector::mul),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn div(&self, arg: &Entry) -> CalculatorResult<Entry> {
|
|
||||||
match arg {
|
|
||||||
Entry::Matrix(m2) => self.iterated_binary_mat(m2, Vector::div),
|
|
||||||
Entry::Vector(_vector) => Err(CalculatorError::TypeMismatch),
|
|
||||||
Entry::Number(number) => self.iterated_binary_num(number, Vector::div),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn int_divide(&self, arg: &Entry) -> CalculatorResult<Entry> {
|
|
||||||
match arg {
|
|
||||||
Entry::Matrix(m2) => self.iterated_binary_mat(m2, Vector::int_divide),
|
|
||||||
Entry::Vector(_vector) => Err(CalculatorError::TypeMismatch),
|
|
||||||
Entry::Number(number) => self.iterated_binary_num(number, Vector::int_divide),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn modulo(&self, arg: &Entry) -> CalculatorResult<Entry> {
|
|
||||||
match arg {
|
|
||||||
Entry::Matrix(m2) => self.iterated_binary_mat(m2, Vector::modulo),
|
|
||||||
Entry::Vector(_vector) => Err(CalculatorError::TypeMismatch),
|
|
||||||
Entry::Number(number) => self.iterated_binary_num(number, Vector::modulo),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn pow(&self, arg: &Entry) -> CalculatorResult<Entry> {
|
|
||||||
match arg {
|
|
||||||
Entry::Matrix(_m2) => Err(CalculatorError::TypeMismatch),
|
|
||||||
Entry::Vector(_vector) => Err(CalculatorError::TypeMismatch),
|
|
||||||
Entry::Number(number) => self.iterated_binary_num(number, Vector::pow),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl fmt::Display for Matrix {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"[ {} ]",
|
|
||||||
self.vectors
|
|
||||||
.iter()
|
|
||||||
.map(|vector| format!("{}", vector))
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join("; ")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,384 +0,0 @@
|
|||||||
use super::{Entry, Matrix, Vector};
|
|
||||||
use crate::{
|
|
||||||
calc::{
|
|
||||||
entries::CalculatorEntry,
|
|
||||||
errors::{CalculatorError, CalculatorResult},
|
|
||||||
types::{CalculatorAngleMode, CalculatorDisplayMode},
|
|
||||||
},
|
|
||||||
constants,
|
|
||||||
};
|
|
||||||
use rust_decimal::{Decimal, MathematicalOps};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
||||||
pub struct Number {
|
|
||||||
pub value: Decimal,
|
|
||||||
}
|
|
||||||
|
|
||||||
// impl PartialEq for Number {
|
|
||||||
// fn eq(&self, other: &Self) -> bool {
|
|
||||||
// (self.value - other.value).abs() < f64::EPSILON
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
impl CalculatorEntry for Number {
|
|
||||||
fn to_editable_string(&self) -> CalculatorResult<String> {
|
|
||||||
Ok(format!("{}", self.value))
|
|
||||||
}
|
|
||||||
fn format_entry(&self, display_mode: &CalculatorDisplayMode) -> String {
|
|
||||||
match display_mode {
|
|
||||||
CalculatorDisplayMode::Default => format!("{}", self.value),
|
|
||||||
CalculatorDisplayMode::Separated { separator } => separated(self.value, *separator),
|
|
||||||
CalculatorDisplayMode::Scientific { precision } => scientific(self.value, *precision),
|
|
||||||
CalculatorDisplayMode::Engineering { precision } => engineering(self.value, *precision),
|
|
||||||
CalculatorDisplayMode::Fixed { precision } => {
|
|
||||||
format!("{:0>.precision$}", self.value, precision = precision)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn is_valid(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
fn validate(self) -> CalculatorResult<Entry> {
|
|
||||||
if self.is_valid() {
|
|
||||||
Ok(Entry::Number(self))
|
|
||||||
} else {
|
|
||||||
Err(CalculatorError::ArithmeticError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn negate(&self) -> CalculatorResult<Entry> {
|
|
||||||
Ok(Entry::Number(Self { value: -self.value }))
|
|
||||||
}
|
|
||||||
fn abs(&self) -> CalculatorResult<Entry> {
|
|
||||||
Ok(Entry::Number(Self {
|
|
||||||
value: self.value.abs(),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
fn inverse(&self) -> CalculatorResult<Entry> {
|
|
||||||
Self {
|
|
||||||
value: constants::ONE
|
|
||||||
.checked_div(self.value)
|
|
||||||
.ok_or(CalculatorError::ArithmeticError)?,
|
|
||||||
}
|
|
||||||
.validate()
|
|
||||||
}
|
|
||||||
fn transpose(&self) -> CalculatorResult<Entry> {
|
|
||||||
Err(CalculatorError::TypeMismatch)
|
|
||||||
}
|
|
||||||
fn sin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
|
|
||||||
Ok(Entry::Number(Self {
|
|
||||||
value: match angle_mode {
|
|
||||||
CalculatorAngleMode::Degrees => (self.value * constants::DEC_TO_RAD_MULTIPLIER)
|
|
||||||
.checked_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> {
|
|
||||||
Ok(Entry::Number(Self {
|
|
||||||
value: match angle_mode {
|
|
||||||
CalculatorAngleMode::Degrees => (self.value * constants::DEC_TO_RAD_MULTIPLIER)
|
|
||||||
.checked_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> {
|
|
||||||
Ok(Entry::Number(Self {
|
|
||||||
value: match angle_mode {
|
|
||||||
CalculatorAngleMode::Degrees => (self.value * constants::DEC_TO_RAD_MULTIPLIER)
|
|
||||||
.checked_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> {
|
|
||||||
// TODO: Implement this
|
|
||||||
Err(CalculatorError::NotYetImplemented)
|
|
||||||
// Ok(Entry::Number(Self {
|
|
||||||
// value: match angle_mode {
|
|
||||||
// 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> {
|
|
||||||
// TODO: Implement this
|
|
||||||
Err(CalculatorError::NotYetImplemented)
|
|
||||||
// Ok(Entry::Number(Self {
|
|
||||||
// value: match angle_mode {
|
|
||||||
// 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> {
|
|
||||||
// TODO: Implement this
|
|
||||||
Err(CalculatorError::NotYetImplemented)
|
|
||||||
// Ok(Entry::Number(Self {
|
|
||||||
// value: match angle_mode {
|
|
||||||
// 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> {
|
|
||||||
Ok(Entry::Number(Self {
|
|
||||||
value: self.value.sqrt().ok_or(CalculatorError::ArithmeticError)?,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
fn log(&self) -> CalculatorResult<Entry> {
|
|
||||||
Ok(Entry::Number(Self {
|
|
||||||
value: self
|
|
||||||
.value
|
|
||||||
.checked_log10()
|
|
||||||
.ok_or(CalculatorError::ArithmeticError)?,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
fn ln(&self) -> CalculatorResult<Entry> {
|
|
||||||
Ok(Entry::Number(Self {
|
|
||||||
value: self
|
|
||||||
.value
|
|
||||||
.checked_ln()
|
|
||||||
.ok_or(CalculatorError::ArithmeticError)?,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add(&self, arg: &Entry) -> CalculatorResult<Entry> {
|
|
||||||
match arg {
|
|
||||||
Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::add),
|
|
||||||
Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::add),
|
|
||||||
Entry::Number(number) => Self {
|
|
||||||
value: self.value + number.value,
|
|
||||||
}
|
|
||||||
.validate(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn sub(&self, arg: &Entry) -> CalculatorResult<Entry> {
|
|
||||||
match arg {
|
|
||||||
Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::sub),
|
|
||||||
Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::sub),
|
|
||||||
Entry::Number(number) => Self {
|
|
||||||
value: self.value - number.value,
|
|
||||||
}
|
|
||||||
.validate(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn mul(&self, arg: &Entry) -> CalculatorResult<Entry> {
|
|
||||||
match arg {
|
|
||||||
Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::mul),
|
|
||||||
Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::mul),
|
|
||||||
Entry::Number(number) => Self {
|
|
||||||
value: self.value * number.value,
|
|
||||||
}
|
|
||||||
.validate(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn div(&self, arg: &Entry) -> CalculatorResult<Entry> {
|
|
||||||
match arg {
|
|
||||||
Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::div),
|
|
||||||
Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::div),
|
|
||||||
Entry::Number(number) => Self {
|
|
||||||
value: self.value / number.value,
|
|
||||||
}
|
|
||||||
.validate(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn int_divide(&self, arg: &Entry) -> CalculatorResult<Entry> {
|
|
||||||
match arg {
|
|
||||||
Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::int_divide),
|
|
||||||
Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::int_divide),
|
|
||||||
Entry::Number(number) => {
|
|
||||||
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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn modulo(&self, arg: &Entry) -> CalculatorResult<Entry> {
|
|
||||||
match arg {
|
|
||||||
Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::modulo),
|
|
||||||
Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::modulo),
|
|
||||||
Entry::Number(number) => Self {
|
|
||||||
value: self.value % number.value,
|
|
||||||
}
|
|
||||||
.validate(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn pow(&self, arg: &Entry) -> CalculatorResult<Entry> {
|
|
||||||
match arg {
|
|
||||||
Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::pow),
|
|
||||||
Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::pow),
|
|
||||||
Entry::Number(number) => Self {
|
|
||||||
value: self
|
|
||||||
.value
|
|
||||||
.checked_powd(number.value)
|
|
||||||
.ok_or(CalculatorError::ArithmeticError)?,
|
|
||||||
}
|
|
||||||
.validate(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Number {
|
|
||||||
pub const ZERO: Self = Self {
|
|
||||||
value: constants::ZERO,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn iterated_binary_vec(
|
|
||||||
self,
|
|
||||||
vector: &Vector,
|
|
||||||
op: impl Fn(&Self, &Entry) -> CalculatorResult<Entry>,
|
|
||||||
) -> CalculatorResult<Entry> {
|
|
||||||
Ok(Entry::Vector(Vector {
|
|
||||||
values: vector
|
|
||||||
.values
|
|
||||||
.iter()
|
|
||||||
.map(|n| op(&self, &Entry::Number(*n)))
|
|
||||||
.map(|e| match e {
|
|
||||||
// Only numbers are valid in a vector
|
|
||||||
Ok(Entry::Number(number)) => Ok(number),
|
|
||||||
_ => Err(CalculatorError::ArithmeticError),
|
|
||||||
})
|
|
||||||
.collect::<CalculatorResult<Vec<Self>>>()?,
|
|
||||||
direction: vector.direction,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn iterated_binary_mat(
|
|
||||||
self,
|
|
||||||
matrix: &Matrix,
|
|
||||||
op: impl Fn(&Self, &Entry) -> CalculatorResult<Entry>,
|
|
||||||
) -> CalculatorResult<Entry> {
|
|
||||||
Matrix {
|
|
||||||
vectors: matrix
|
|
||||||
.vectors
|
|
||||||
.iter()
|
|
||||||
.map(|v| op(&self, &Entry::Vector(v.clone())))
|
|
||||||
.map(|e| match e {
|
|
||||||
// Only numbers are valid in a vector
|
|
||||||
Ok(Entry::Vector(vector)) => Ok(vector),
|
|
||||||
_ => Err(CalculatorError::ArithmeticError),
|
|
||||||
})
|
|
||||||
.collect::<CalculatorResult<Vec<Vector>>>()?,
|
|
||||||
dimensions: matrix.dimensions.clone(),
|
|
||||||
}
|
|
||||||
.validate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl fmt::Display for Number {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}", self.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Based on https://stackoverflow.com/a/65266882
|
|
||||||
fn scientific(f: Decimal, precision: usize) -> String {
|
|
||||||
let mut ret = format!("{:.precision$E}", f, precision = precision);
|
|
||||||
let exp = ret.split_off(ret.find('E').unwrap_or(0));
|
|
||||||
let (exp_sign, exp) = exp
|
|
||||||
.strip_prefix("E-")
|
|
||||||
.map_or_else(|| ('+', &exp[1..]), |stripped| ('-', stripped));
|
|
||||||
|
|
||||||
let sign = if ret.starts_with('-') { "" } else { " " };
|
|
||||||
format!("{}{} E{}{:0>pad$}", sign, ret, exp_sign, exp, pad = 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
// 1,000 => 1000E3
|
|
||||||
let all = format!(" {:.precision$E}", f, precision = precision)
|
|
||||||
// Remove . since it can be moved
|
|
||||||
.replacen(".", "", 1)
|
|
||||||
// Add 00E before E here so the length is enough for slicing below
|
|
||||||
.replacen("E", "00E", 1);
|
|
||||||
// Extract mantissa and the string representation of the exponent. Unwrap should be safe as formatter will insert E
|
|
||||||
// 1000E3 => (1000, E3)
|
|
||||||
let (num_str, exp_str) = all.split_at(all.find('E').unwrap());
|
|
||||||
// Extract the exponent as an isize. This should always be true because Entry max will be ~400
|
|
||||||
// E3 => 3 as isize
|
|
||||||
let exp = exp_str[1..].parse::<isize>().unwrap();
|
|
||||||
// Sign of the exponent. If string representation starts with E-, then negative
|
|
||||||
let display_exp_sign = if exp_str.strip_prefix("E-").is_some() {
|
|
||||||
'-'
|
|
||||||
} else {
|
|
||||||
'+'
|
|
||||||
};
|
|
||||||
|
|
||||||
// The exponent to display. Always a multiple of 3 in engineering mode. Always positive because sign is added with display_exp_sign above
|
|
||||||
// 100 => 0, 1000 => 3, .1 => 3 (but will show as -3)
|
|
||||||
let display_exp = (exp.div_euclid(3) * 3).abs();
|
|
||||||
// Number of whole digits. Always 1, 2, or 3 depending on exponent divisibility
|
|
||||||
let num_whole_digits = exp.rem_euclid(3) as usize + 1;
|
|
||||||
|
|
||||||
// If this is a negative number, strip off the added space, otherwise keep the space (and next digit)
|
|
||||||
let num_str = if num_str.strip_prefix(" -").is_some() {
|
|
||||||
&num_str[1..]
|
|
||||||
} else {
|
|
||||||
num_str
|
|
||||||
};
|
|
||||||
|
|
||||||
// Whole portion of number. Slice is safe because the num_whole_digits is always 3 and the num_str will always have length >= 3 since precision in all=2 (+original whole digit)
|
|
||||||
// Original number is 1,000 => whole will be 1, if original is 0.01, whole will be 10
|
|
||||||
let whole = &num_str[0..=num_whole_digits];
|
|
||||||
// Decimal portion of the number. Sliced from the number of whole digits to the *requested* precision. Precision generated in all will be requested precision + 2
|
|
||||||
let decimal = &num_str[(num_whole_digits + 1)..=(precision + num_whole_digits)];
|
|
||||||
// Right align whole portion, always have decimal point
|
|
||||||
format!(
|
|
||||||
"{: >4}.{} E{}{:0>pad$}",
|
|
||||||
// display_sign,
|
|
||||||
whole,
|
|
||||||
decimal,
|
|
||||||
display_exp_sign,
|
|
||||||
display_exp,
|
|
||||||
pad = 2
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn separated(f: Decimal, sep: char) -> String {
|
|
||||||
let mut ret = f.to_string();
|
|
||||||
let start = if ret.starts_with('-') { 1 } else { 0 };
|
|
||||||
let end = ret.find('.').unwrap_or_else(|| ret.len());
|
|
||||||
for i in 0..((end - start - 1).div_euclid(3)) {
|
|
||||||
ret.insert(end - (i + 1) * 3, sep);
|
|
||||||
}
|
|
||||||
ret
|
|
||||||
}
|
|
@ -1,296 +0,0 @@
|
|||||||
use super::{Entry, Matrix, Number};
|
|
||||||
use crate::{
|
|
||||||
calc::{
|
|
||||||
entries::CalculatorEntry,
|
|
||||||
errors::{CalculatorError, CalculatorResult},
|
|
||||||
types::{CalculatorAngleMode, CalculatorDisplayMode},
|
|
||||||
},
|
|
||||||
constants,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
#[derive(PartialEq, Copy, Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub enum VectorDirection {
|
|
||||||
Row,
|
|
||||||
Column,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VectorDirection {
|
|
||||||
pub const fn swap(&self) -> Self {
|
|
||||||
match self {
|
|
||||||
Self::Row => Self::Column,
|
|
||||||
Self::Column => Self::Row,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn get_separator(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Self::Row => " ",
|
|
||||||
Self::Column => "; ",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for VectorDirection {
|
|
||||||
fn default() -> Self {
|
|
||||||
// Column vectors are the default
|
|
||||||
Self::Column
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Vector {
|
|
||||||
pub values: Vec<Number>,
|
|
||||||
pub direction: VectorDirection,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CalculatorEntry for Vector {
|
|
||||||
// Misc
|
|
||||||
fn to_editable_string(&self) -> CalculatorResult<String> {
|
|
||||||
// TODO: Eventualy we can parse and edit a vector as a string
|
|
||||||
Err(CalculatorError::TypeMismatch)
|
|
||||||
}
|
|
||||||
fn is_valid(&self) -> bool {
|
|
||||||
self.values.iter().all(|number| number.is_valid())
|
|
||||||
}
|
|
||||||
fn validate(self) -> CalculatorResult<Entry> {
|
|
||||||
if self.is_valid() {
|
|
||||||
Ok(Entry::Vector(self))
|
|
||||||
} else {
|
|
||||||
Err(CalculatorError::ArithmeticError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_entry(&self, display_mode: &CalculatorDisplayMode) -> String {
|
|
||||||
format!(
|
|
||||||
"[{}]",
|
|
||||||
self.values
|
|
||||||
.iter()
|
|
||||||
.map(|number| number.format_entry(display_mode))
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(self.direction.get_separator())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// Mathematical operations
|
|
||||||
fn negate(&self) -> CalculatorResult<Entry> {
|
|
||||||
self.iterated_unary(Number::negate)
|
|
||||||
}
|
|
||||||
fn abs(&self) -> CalculatorResult<Entry> {
|
|
||||||
let value: Entry =
|
|
||||||
self.values
|
|
||||||
.iter()
|
|
||||||
.try_fold(Entry::Number(Number::ZERO), |acc, n2| {
|
|
||||||
acc.add(&n2.pow(&Entry::Number(Number {
|
|
||||||
value: constants::TWO,
|
|
||||||
}))?)
|
|
||||||
})?;
|
|
||||||
value.sqrt()
|
|
||||||
}
|
|
||||||
fn inverse(&self) -> CalculatorResult<Entry> {
|
|
||||||
Err(CalculatorError::TypeMismatch)
|
|
||||||
}
|
|
||||||
fn transpose(&self) -> CalculatorResult<Entry> {
|
|
||||||
Ok(Entry::Vector(Self {
|
|
||||||
values: self.values.clone(),
|
|
||||||
direction: self.direction.swap(),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
fn sin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
|
|
||||||
self.iterated_unary(|n| n.sin(angle_mode))
|
|
||||||
}
|
|
||||||
fn cos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
|
|
||||||
self.iterated_unary(|n| n.cos(angle_mode))
|
|
||||||
}
|
|
||||||
fn tan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
|
|
||||||
self.iterated_unary(|n| n.tan(angle_mode))
|
|
||||||
}
|
|
||||||
fn asin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
|
|
||||||
self.iterated_unary(|n| n.asin(angle_mode))
|
|
||||||
}
|
|
||||||
fn acos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
|
|
||||||
self.iterated_unary(|n| n.acos(angle_mode))
|
|
||||||
}
|
|
||||||
fn atan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
|
|
||||||
self.iterated_unary(|n| n.atan(angle_mode))
|
|
||||||
}
|
|
||||||
fn sqrt(&self) -> CalculatorResult<Entry> {
|
|
||||||
self.iterated_unary(Number::sqrt)
|
|
||||||
}
|
|
||||||
fn log(&self) -> CalculatorResult<Entry> {
|
|
||||||
self.iterated_unary(Number::log)
|
|
||||||
}
|
|
||||||
fn ln(&self) -> CalculatorResult<Entry> {
|
|
||||||
self.iterated_unary(Number::ln)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add(&self, arg: &Entry) -> CalculatorResult<Entry> {
|
|
||||||
match arg {
|
|
||||||
Entry::Matrix(_) => Err(CalculatorError::TypeMismatch),
|
|
||||||
Entry::Vector(v2) => self.iterated_binary_vec(v2, Number::add),
|
|
||||||
Entry::Number(number) => self.iterated_binary_num(number, Number::add),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn sub(&self, arg: &Entry) -> CalculatorResult<Entry> {
|
|
||||||
match arg {
|
|
||||||
Entry::Matrix(_) => Err(CalculatorError::TypeMismatch),
|
|
||||||
Entry::Vector(v2) => self.iterated_binary_vec(v2, Number::sub),
|
|
||||||
Entry::Number(number) => self.iterated_binary_num(number, Number::sub),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn mul(&self, arg: &Entry) -> CalculatorResult<Entry> {
|
|
||||||
match arg {
|
|
||||||
Entry::Matrix(_matrix) => Matrix::from(&[Entry::Vector(self.clone())])?.mul(arg),
|
|
||||||
Entry::Vector(v2) => {
|
|
||||||
if self.values.len() != v2.values.len() {
|
|
||||||
return Err(CalculatorError::DimensionMismatch);
|
|
||||||
}
|
|
||||||
match (self.direction, v2.direction) {
|
|
||||||
(VectorDirection::Row, VectorDirection::Column) => {
|
|
||||||
// Row by column -- will produce a scalar
|
|
||||||
self.values
|
|
||||||
.iter()
|
|
||||||
.zip(v2.values.iter())
|
|
||||||
.try_fold(Entry::Number(Number::ZERO), |acc, (n1, n2)| {
|
|
||||||
acc.add(&n1.mul(&Entry::Number(*n2))?)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
(VectorDirection::Column, VectorDirection::Row) => {
|
|
||||||
Matrix::from(&[Entry::Vector(self.clone())])?
|
|
||||||
.mul(&Matrix::from(&[arg.clone()])?)
|
|
||||||
}
|
|
||||||
(VectorDirection::Row, VectorDirection::Row)
|
|
||||||
| (VectorDirection::Column, VectorDirection::Column) => {
|
|
||||||
Err(CalculatorError::DimensionMismatch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Entry::Number(number) => self.iterated_binary_num(number, Number::mul),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn div(&self, arg: &Entry) -> CalculatorResult<Entry> {
|
|
||||||
match arg {
|
|
||||||
Entry::Matrix(_) => Err(CalculatorError::TypeMismatch),
|
|
||||||
Entry::Vector(v2) => self.iterated_binary_vec(v2, Number::div),
|
|
||||||
Entry::Number(number) => self.iterated_binary_num(number, Number::div),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn int_divide(&self, arg: &Entry) -> CalculatorResult<Entry> {
|
|
||||||
match arg {
|
|
||||||
Entry::Matrix(_) => Err(CalculatorError::TypeMismatch),
|
|
||||||
Entry::Vector(v2) => self.iterated_binary_vec(v2, Number::int_divide),
|
|
||||||
Entry::Number(number) => self.iterated_binary_num(number, Number::int_divide),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn modulo(&self, arg: &Entry) -> CalculatorResult<Entry> {
|
|
||||||
match arg {
|
|
||||||
Entry::Matrix(_) => Err(CalculatorError::TypeMismatch),
|
|
||||||
Entry::Vector(v2) => self.iterated_binary_vec(v2, Number::modulo),
|
|
||||||
Entry::Number(number) => self.iterated_binary_num(number, Number::modulo),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn pow(&self, arg: &Entry) -> CalculatorResult<Entry> {
|
|
||||||
match arg {
|
|
||||||
Entry::Matrix(_) => Err(CalculatorError::TypeMismatch),
|
|
||||||
Entry::Vector(v2) => self.iterated_binary_vec(v2, Number::pow),
|
|
||||||
Entry::Number(number) => self.iterated_binary_num(number, Number::pow),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Vector {
|
|
||||||
pub fn from(entries: &[Entry]) -> CalculatorResult<Entry> {
|
|
||||||
if entries.is_empty() {
|
|
||||||
return Err(CalculatorError::NotEnoughStackEntries);
|
|
||||||
}
|
|
||||||
Self {
|
|
||||||
values: entries
|
|
||||||
.iter()
|
|
||||||
.map(|e| match e {
|
|
||||||
Entry::Matrix(_) | Entry::Vector(_) => Err(CalculatorError::TypeMismatch),
|
|
||||||
Entry::Number(number) => Ok(*number),
|
|
||||||
})
|
|
||||||
.collect::<CalculatorResult<Vec<Number>>>()?,
|
|
||||||
direction: VectorDirection::default(),
|
|
||||||
}
|
|
||||||
.validate()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn iterated_unary(
|
|
||||||
&self,
|
|
||||||
op: impl Fn(&Number) -> CalculatorResult<Entry>,
|
|
||||||
) -> CalculatorResult<Entry> {
|
|
||||||
Ok(Entry::Vector(Self {
|
|
||||||
values: self
|
|
||||||
.values
|
|
||||||
.iter()
|
|
||||||
.map(|n| op(n))
|
|
||||||
.map(|e| match e {
|
|
||||||
// Only numbers are valid in a vector
|
|
||||||
Ok(Entry::Number(number)) => Ok(number),
|
|
||||||
_ => Err(CalculatorError::ArithmeticError),
|
|
||||||
})
|
|
||||||
.collect::<CalculatorResult<Vec<Number>>>()?,
|
|
||||||
direction: self.direction,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn iterated_binary_vec(
|
|
||||||
&self,
|
|
||||||
v2: &Self,
|
|
||||||
op: impl Fn(&Number, &Entry) -> CalculatorResult<Entry>,
|
|
||||||
) -> CalculatorResult<Entry> {
|
|
||||||
if self.values.len() != v2.values.len() {
|
|
||||||
return Err(CalculatorError::DimensionMismatch);
|
|
||||||
}
|
|
||||||
if self.direction != v2.direction {
|
|
||||||
return Err(CalculatorError::DimensionMismatch);
|
|
||||||
}
|
|
||||||
Ok(Entry::Vector(Self {
|
|
||||||
values: self
|
|
||||||
.values
|
|
||||||
.iter()
|
|
||||||
.zip(v2.values.iter())
|
|
||||||
.map(|(n1, n2)| op(n1, &Entry::Number(*n2)))
|
|
||||||
.map(|e| match e {
|
|
||||||
// Only numbers are valid in a vector
|
|
||||||
Ok(Entry::Number(number)) => Ok(number),
|
|
||||||
_ => Err(CalculatorError::ArithmeticError),
|
|
||||||
})
|
|
||||||
.collect::<CalculatorResult<Vec<Number>>>()?,
|
|
||||||
direction: self.direction,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
fn iterated_binary_num(
|
|
||||||
&self,
|
|
||||||
n2: &Number,
|
|
||||||
op: impl Fn(&Number, &Entry) -> CalculatorResult<Entry>,
|
|
||||||
) -> CalculatorResult<Entry> {
|
|
||||||
Ok(Entry::Vector(Self {
|
|
||||||
values: self
|
|
||||||
.values
|
|
||||||
.iter()
|
|
||||||
.map(|n| op(n, &Entry::Number(*n2)))
|
|
||||||
.map(|e| match e {
|
|
||||||
// Only numbers are valid in a vector
|
|
||||||
Ok(Entry::Number(number)) => Ok(number),
|
|
||||||
_ => Err(CalculatorError::ArithmeticError),
|
|
||||||
})
|
|
||||||
.collect::<CalculatorResult<Vec<Number>>>()?,
|
|
||||||
direction: self.direction,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl fmt::Display for Vector {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"[{}]",
|
|
||||||
self.values
|
|
||||||
.iter()
|
|
||||||
.map(|number| format!("{}", number))
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join("; ")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,46 +1,24 @@
|
|||||||
use confy::ConfyError;
|
use confy::ConfyError;
|
||||||
use std::{error, fmt};
|
use std::error;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
pub type CalculatorResult<T> = Result<T, CalculatorError>;
|
pub type CalculatorResult<T> = Result<T, CalculatorError>;
|
||||||
|
|
||||||
/// All possible errors the calculator can throw
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum CalculatorError {
|
pub enum CalculatorError {
|
||||||
/// Divide by zero, log(-1), etc
|
|
||||||
ArithmeticError,
|
ArithmeticError,
|
||||||
/// Not enough stck entries for operation
|
|
||||||
NotEnoughStackEntries,
|
NotEnoughStackEntries,
|
||||||
/// Requested type does not match target type
|
|
||||||
TypeMismatch,
|
|
||||||
/// Dimensions must match
|
|
||||||
DimensionMismatch,
|
|
||||||
/// Plans are in place to implement, but we are not there yet
|
|
||||||
NotYetImplemented,
|
|
||||||
/// Internal computation error
|
|
||||||
InternalError(String),
|
|
||||||
/// Thrown when an undo or redo cannot be performed
|
|
||||||
CorruptStateChange(String),
|
CorruptStateChange(String),
|
||||||
/// Cannot undo or redo
|
|
||||||
EmptyHistory(String),
|
EmptyHistory(String),
|
||||||
/// Constant undefined
|
|
||||||
NoSuchConstant(char),
|
NoSuchConstant(char),
|
||||||
/// Register undefined
|
|
||||||
NoSuchRegister(char),
|
NoSuchRegister(char),
|
||||||
/// Macro undefined
|
|
||||||
NoSuchMacro(char),
|
NoSuchMacro(char),
|
||||||
/// Operator undefined
|
|
||||||
NoSuchOperator(char),
|
NoSuchOperator(char),
|
||||||
/// Setting undefined
|
|
||||||
NoSuchSetting(char),
|
NoSuchSetting(char),
|
||||||
/// Macro calls itself
|
|
||||||
RecursiveMacro(char),
|
RecursiveMacro(char),
|
||||||
/// Could not convert l to number
|
|
||||||
ParseError,
|
ParseError,
|
||||||
/// Requested precision is too high
|
|
||||||
PrecisionTooHigh,
|
PrecisionTooHigh,
|
||||||
/// Config serialization error
|
|
||||||
SaveError(Option<ConfyError>),
|
SaveError(Option<ConfyError>),
|
||||||
/// Config deserialization error
|
|
||||||
LoadError(Option<ConfyError>),
|
LoadError(Option<ConfyError>),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,36 +27,30 @@ impl error::Error for CalculatorError {}
|
|||||||
impl fmt::Display for CalculatorError {
|
impl fmt::Display for CalculatorError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::ArithmeticError => write!(f, "Arithmetic Error"),
|
CalculatorError::ArithmeticError => write!(f, "Arithmetic Error"),
|
||||||
Self::NotEnoughStackEntries => write!(f, "Not enough items in the stack"),
|
CalculatorError::NotEnoughStackEntries => write!(f, "Not enough items in the stack"),
|
||||||
Self::TypeMismatch => write!(f, "Type mismatch"),
|
CalculatorError::CorruptStateChange(msg) => {
|
||||||
Self::DimensionMismatch => write!(f, "Dimension mismatch"),
|
|
||||||
Self::NotYetImplemented => write!(f, "Not yet implemented"),
|
|
||||||
Self::InternalError(msg) => {
|
|
||||||
write!(f, "Internal error: {}", msg)
|
|
||||||
}
|
|
||||||
Self::CorruptStateChange(msg) => {
|
|
||||||
write!(f, "Corrupt state change: {}", msg)
|
write!(f, "Corrupt state change: {}", msg)
|
||||||
}
|
}
|
||||||
Self::EmptyHistory(msg) => write!(f, "No history to {}", msg),
|
CalculatorError::EmptyHistory(msg) => write!(f, "No history to {}", msg),
|
||||||
Self::NoSuchOperator(c) => write!(f, "No such operator '{}'", c),
|
CalculatorError::NoSuchOperator(c) => write!(f, "No such operator '{}'", c),
|
||||||
Self::NoSuchConstant(c) => write!(f, "No such constant '{}'", c),
|
CalculatorError::NoSuchConstant(c) => write!(f, "No such constant '{}'", c),
|
||||||
Self::NoSuchRegister(c) => write!(f, "No such register '{}'", c),
|
CalculatorError::NoSuchRegister(c) => write!(f, "No such register '{}'", c),
|
||||||
Self::NoSuchMacro(c) => write!(f, "No such macro '{}'", c),
|
CalculatorError::NoSuchMacro(c) => write!(f, "No such macro '{}'", c),
|
||||||
Self::NoSuchSetting(c) => write!(f, "No such setting '{}'", c),
|
CalculatorError::NoSuchSetting(c) => write!(f, "No such setting '{}'", c),
|
||||||
Self::RecursiveMacro(c) => write!(f, "Recursive macro '{}'", c),
|
CalculatorError::RecursiveMacro(c) => write!(f, "Recursive macro '{}'", c),
|
||||||
Self::ParseError => write!(f, "Parse error"),
|
CalculatorError::ParseError => write!(f, "Parse error"),
|
||||||
Self::PrecisionTooHigh => write!(f, "Precision too high"),
|
CalculatorError::PrecisionTooHigh => write!(f, "Precision too high"),
|
||||||
Self::SaveError(None) => write!(f, "Could not save"),
|
CalculatorError::SaveError(None) => write!(f, "Could not save"),
|
||||||
Self::SaveError(Some(ConfyError::SerializeYamlError(e))) => {
|
CalculatorError::SaveError(Some(ConfyError::SerializeTomlError(e))) => {
|
||||||
write!(f, "Save serialization error: {}", e)
|
write!(f, "Save serialization error: {}", e)
|
||||||
}
|
}
|
||||||
Self::SaveError(Some(e)) => write!(f, "Could not save: {}", e),
|
CalculatorError::SaveError(Some(e)) => write!(f, "Could not save: {}", e),
|
||||||
Self::LoadError(None) => write!(f, "Could not load"),
|
CalculatorError::LoadError(None) => write!(f, "Could not load"),
|
||||||
Self::LoadError(Some(ConfyError::SerializeYamlError(e))) => {
|
CalculatorError::LoadError(Some(ConfyError::SerializeTomlError(e))) => {
|
||||||
write!(f, "Load serialization error: {}", e)
|
write!(f, "Load serialization error: {}", e)
|
||||||
}
|
}
|
||||||
Self::LoadError(Some(e)) => write!(f, "Could not load: {}", e),
|
CalculatorError::LoadError(Some(e)) => write!(f, "Could not load: {}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
use super::entries::Entry;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
#[derive(PartialEq, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum MacroState {
|
||||||
|
Start,
|
||||||
|
End,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Debug, Serialize, Deserialize, Copy, Clone)]
|
#[derive(PartialEq, Debug, Serialize, Deserialize)]
|
||||||
pub enum ArithmeticOperation {
|
pub enum CalculatorOperation {
|
||||||
Add,
|
Add,
|
||||||
Subtract,
|
Subtract,
|
||||||
Multiply,
|
Multiply,
|
||||||
@ -12,6 +16,10 @@ pub enum ArithmeticOperation {
|
|||||||
Inverse,
|
Inverse,
|
||||||
Modulo,
|
Modulo,
|
||||||
IntegerDivide,
|
IntegerDivide,
|
||||||
|
//Remainder,
|
||||||
|
Drop,
|
||||||
|
Dup,
|
||||||
|
Swap,
|
||||||
Sin,
|
Sin,
|
||||||
Cos,
|
Cos,
|
||||||
Tan,
|
Tan,
|
||||||
@ -19,49 +27,24 @@ pub enum ArithmeticOperation {
|
|||||||
ACos,
|
ACos,
|
||||||
ATan,
|
ATan,
|
||||||
Sqrt,
|
Sqrt,
|
||||||
Pow,
|
|
||||||
Log,
|
|
||||||
Ln,
|
|
||||||
}
|
|
||||||
/// Operations that can be sent to the calculator such as +, -, or undo
|
|
||||||
#[derive(PartialEq, Debug, Serialize, Deserialize, Copy, Clone)]
|
|
||||||
pub enum CalculatorOperation {
|
|
||||||
ArithmeticOperation(ArithmeticOperation),
|
|
||||||
BuildVector,
|
|
||||||
BuildMatrix,
|
|
||||||
Deconstruct,
|
|
||||||
Transpose,
|
|
||||||
Undo,
|
Undo,
|
||||||
Redo,
|
Redo,
|
||||||
Drop,
|
Pow,
|
||||||
Dup,
|
// Factorial,
|
||||||
Swap,
|
Log,
|
||||||
|
Ln,
|
||||||
|
E,
|
||||||
Macro(MacroState),
|
Macro(MacroState),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Macro bundary; defined by the start or end of a macro invocation
|
|
||||||
#[derive(PartialEq, Debug, Serialize, Deserialize, Copy, Clone)]
|
|
||||||
pub enum MacroState {
|
|
||||||
Start,
|
|
||||||
End,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Arguments for a given operation
|
|
||||||
#[derive(PartialEq, Debug, Serialize, Deserialize)]
|
#[derive(PartialEq, Debug, Serialize, Deserialize)]
|
||||||
pub enum OpArgs {
|
pub enum OpArgs {
|
||||||
/// This is a macro start and end noop
|
|
||||||
Macro(MacroState),
|
Macro(MacroState),
|
||||||
/// Operation takes 1 argument, ex: sqrt or negate
|
Unary(f64),
|
||||||
Unary(Entry),
|
Binary([f64; 2]),
|
||||||
/// Operation takes 2 arguments, ex: + or -
|
|
||||||
Binary([Entry; 2]),
|
|
||||||
/// Some variable number of changes
|
|
||||||
Variable(Vec<Entry>),
|
|
||||||
/// Operation takes no arguments, ex: push
|
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Record of what to pop and push. Used for undo and redo buffers
|
|
||||||
#[derive(PartialEq, Debug, Serialize, Deserialize)]
|
#[derive(PartialEq, Debug, Serialize, Deserialize)]
|
||||||
pub struct CalculatorStateChange {
|
pub struct CalculatorStateChange {
|
||||||
pub pop: OpArgs,
|
pub pop: OpArgs,
|
||||||
|
@ -1,224 +0,0 @@
|
|||||||
use super::entries::Entry;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::{collections::HashMap, fmt};
|
|
||||||
|
|
||||||
/// The calculator state
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub enum CalculatorState {
|
|
||||||
Normal,
|
|
||||||
WaitingForConstant,
|
|
||||||
WaitingForMacro,
|
|
||||||
WaitingForRegister(RegisterState),
|
|
||||||
WaitingForSetting,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for CalculatorState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Normal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The state of the requested register operation
|
|
||||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
|
||||||
pub enum RegisterState {
|
|
||||||
Save,
|
|
||||||
Load,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One calculator constant containing a message and value
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct CalculatorConstant {
|
|
||||||
pub help: String,
|
|
||||||
pub value: Entry,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One calculator macro containing a messsage and value
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct CalculatorMacro {
|
|
||||||
pub help: String,
|
|
||||||
pub value: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Map of chars to constants
|
|
||||||
pub type CalculatorConstants = HashMap<char, CalculatorConstant>;
|
|
||||||
|
|
||||||
/// Map of chars to macros
|
|
||||||
pub type CalculatorMacros = HashMap<char, CalculatorMacro>;
|
|
||||||
|
|
||||||
/// Map of chars to registers
|
|
||||||
pub type CalculatorRegisters = HashMap<char, Entry>;
|
|
||||||
|
|
||||||
/// Possible calculator angle modes
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
|
||||||
#[serde(tag = "angle_mode")]
|
|
||||||
pub enum CalculatorAngleMode {
|
|
||||||
Degrees,
|
|
||||||
Radians,
|
|
||||||
Grads,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for CalculatorAngleMode {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Degrees
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for CalculatorAngleMode {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Degrees => write!(f, "DEG"),
|
|
||||||
Self::Radians => write!(f, "RAD"),
|
|
||||||
Self::Grads => write!(f, "GRD"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The calculator digit display mode
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
#[serde(tag = "display_mode")]
|
|
||||||
pub enum CalculatorDisplayMode {
|
|
||||||
/// Rust's default Entry format
|
|
||||||
Default,
|
|
||||||
/// Thousands separator
|
|
||||||
Separated { separator: char },
|
|
||||||
/// Aligned scientific format
|
|
||||||
Scientific { precision: usize },
|
|
||||||
/// Scientific format, chunked by groups of 3
|
|
||||||
///
|
|
||||||
/// Example: 1 E+5 or 100E+5
|
|
||||||
Engineering { precision: usize },
|
|
||||||
/// Fixed precision
|
|
||||||
Fixed { precision: usize },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for CalculatorDisplayMode {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Default => write!(f, "DEF"),
|
|
||||||
Self::Separated { separator } => write!(f, "SEP({})", separator),
|
|
||||||
Self::Scientific { precision } => write!(f, "SCI({})", precision),
|
|
||||||
Self::Engineering { precision } => write!(f, "ENG({})", precision),
|
|
||||||
Self::Fixed { precision } => write!(f, "FIX({})", precision),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for CalculatorDisplayMode {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CalculatorDisplayMode {}
|
|
||||||
|
|
||||||
/// Left or right calculator alignment
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub enum CalculatorAlignment {
|
|
||||||
Right,
|
|
||||||
Left,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for CalculatorAlignment {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Left
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for CalculatorAlignment {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Left => write!(f, "L"),
|
|
||||||
Self::Right => write!(f, "R"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// #[cfg(test)]
|
|
||||||
// mod tests {
|
|
||||||
// use super::*;
|
|
||||||
// #[test]
|
|
||||||
// fn test_scientific() {
|
|
||||||
// for (f, precision, s) in vec![
|
|
||||||
// // Basic
|
|
||||||
// (1.0, 0, " 1 E+00"),
|
|
||||||
// (-1.0, 0, "-1 E+00"),
|
|
||||||
// (100.0, 0, " 1 E+02"),
|
|
||||||
// (0.1, 0, " 1 E-01"),
|
|
||||||
// (0.01, 0, " 1 E-02"),
|
|
||||||
// (-0.1, 0, "-1 E-01"),
|
|
||||||
// // i
|
|
||||||
// (1.0, 0, " 1 E+00"),
|
|
||||||
// // Precision
|
|
||||||
// (-0.123_456_789, 3, "-1.235 E-01"),
|
|
||||||
// (-0.123_456_789, 2, "-1.23 E-01"),
|
|
||||||
// (-0.123_456_789, 2, "-1.23 E-01"),
|
|
||||||
// (-1e99, 2, "-1.00 E+99"),
|
|
||||||
// (-1e100, 2, "-1.00 E+100"),
|
|
||||||
// // Rounding
|
|
||||||
// (0.5, 2, " 5.00 E-01"),
|
|
||||||
// (0.5, 1, " 5.0 E-01"),
|
|
||||||
// (0.5, 0, " 5 E-01"),
|
|
||||||
// (1.5, 2, " 1.50 E+00"),
|
|
||||||
// (1.5, 1, " 1.5 E+00"),
|
|
||||||
// (1.5, 0, " 2 E+00"),
|
|
||||||
// ] {
|
|
||||||
// assert_eq!(
|
|
||||||
// CalculatorDisplayMode::Scientific { precision }.format_number(f),
|
|
||||||
// s
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[test]
|
|
||||||
// fn test_separated() {
|
|
||||||
// for (f, separator, s) in vec![
|
|
||||||
// (100.0, ',', "100"),
|
|
||||||
// (100.0, ',', "100"),
|
|
||||||
// (-100.0, ',', "-100"),
|
|
||||||
// (1_000.0, ',', "1,000"),
|
|
||||||
// (-1_000.0, ',', "-1,000"),
|
|
||||||
// (10_000.0, ',', "10,000"),
|
|
||||||
// (-10_000.0, ',', "-10,000"),
|
|
||||||
// (100_000.0, ',', "100,000"),
|
|
||||||
// (-100_000.0, ',', "-100,000"),
|
|
||||||
// (1_000_000.0, ',', "1,000,000"),
|
|
||||||
// (-1_000_000.0, ',', "-1,000,000"),
|
|
||||||
// (1_000_000.123_456_789, ',', "1,000,000.123456789"),
|
|
||||||
// (-1_000_000.123_456_789, ',', "-1,000,000.123456789"),
|
|
||||||
// (1_000_000.123_456_789, ' ', "1 000 000.123456789"),
|
|
||||||
// (1_000_000.123_456_789, ' ', "1 000 000.123456789"),
|
|
||||||
// ] {
|
|
||||||
// assert_eq!(
|
|
||||||
// CalculatorDisplayMode::Separated { separator }.format_number(f),
|
|
||||||
// s
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[test]
|
|
||||||
// fn test_engineering() {
|
|
||||||
// for (f, precision, s) in vec![
|
|
||||||
// (100.0, 3, " 100.000 E+00"),
|
|
||||||
// (100.0, 3, " 100.000 E+00"),
|
|
||||||
// (-100.0, 3, "-100.000 E+00"),
|
|
||||||
// (100.0, 0, " 100. E+00"),
|
|
||||||
// (-100.0, 0, "-100. E+00"),
|
|
||||||
// (0.1, 2, " 100.00 E-03"),
|
|
||||||
// (0.01, 2, " 10.00 E-03"),
|
|
||||||
// (0.001, 2, " 1.00 E-03"),
|
|
||||||
// (0.0001, 2, " 100.00 E-06"),
|
|
||||||
// // Rounding
|
|
||||||
// (0.5, 2, " 500.00 E-03"),
|
|
||||||
// (0.5, 1, " 500.0 E-03"),
|
|
||||||
// (0.5, 0, " 500. E-03"),
|
|
||||||
// (1.5, 2, " 1.50 E+00"),
|
|
||||||
// (1.5, 1, " 1.5 E+00"),
|
|
||||||
// (1.5, 0, " 2. E+00"),
|
|
||||||
// ] {
|
|
||||||
// assert_eq!(
|
|
||||||
// CalculatorDisplayMode::Engineering { precision }.format_number(f),
|
|
||||||
// s
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
@ -1,26 +0,0 @@
|
|||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +1,8 @@
|
|||||||
use std::{iter, sync::mpsc};
|
use std::iter;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
|
||||||
use std::{
|
use std::thread;
|
||||||
thread,
|
use std::time::{Duration, Instant};
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crossterm::event::{self, Event as CEvent, KeyEvent};
|
use crossterm::event::{self, Event as CEvent, KeyEvent};
|
||||||
|
|
||||||
@ -12,7 +11,6 @@ pub enum Event<I> {
|
|||||||
Tick,
|
Tick,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct Events {
|
pub struct Events {
|
||||||
rx: mpsc::Receiver<Event<KeyEvent>>,
|
rx: mpsc::Receiver<Event<KeyEvent>>,
|
||||||
tx: mpsc::Sender<Event<KeyEvent>>,
|
tx: mpsc::Sender<Event<KeyEvent>>,
|
||||||
@ -20,7 +18,7 @@ pub struct Events {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Events {
|
impl Events {
|
||||||
pub fn new(tick_rate: u64) -> Self {
|
pub fn new(tick_rate: u64) -> Events {
|
||||||
let (tx, rx) = mpsc::channel();
|
let (tx, rx) = mpsc::channel();
|
||||||
let tick_handle = {
|
let tick_handle = {
|
||||||
let tx = tx.clone();
|
let tx = tx.clone();
|
||||||
@ -45,18 +43,16 @@ impl Events {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
Self {
|
Events {
|
||||||
tx,
|
tx,
|
||||||
rx,
|
rx,
|
||||||
tick_handle,
|
tick_handle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a chain of the next element (blocking) and a `TryIter` of everything after it.
|
/// Returns a chain of the next element (blocking) and a TryIter of everything after it.
|
||||||
/// Useful for reading a single Iterator of all elements once after blocking, so there are not too many UI redraws.
|
/// Useful for reading a single Iterator of all elements once after blocking, so there are not too many UI redraws.
|
||||||
pub fn blocking_iter(
|
pub fn blocking_iter(&self) -> Result<impl Iterator<Item = Event<KeyEvent>> + '_, mpsc::RecvError> {
|
||||||
&self,
|
|
||||||
) -> Result<impl Iterator<Item = Event<KeyEvent>> + '_, mpsc::RecvError> {
|
|
||||||
Ok(iter::once(self.rx.recv()?).chain(self.rx.try_iter()))
|
Ok(iter::once(self.rx.recv()?).chain(self.rx.try_iter()))
|
||||||
}
|
}
|
||||||
}
|
}
|
155
src/format.rs
Normal file
155
src/format.rs
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
// Based on https://stackoverflow.com/a/65266882
|
||||||
|
pub fn scientific(f: f64, precision: usize) -> String {
|
||||||
|
let mut ret = format!("{:.precision$E}", f, precision = precision);
|
||||||
|
let exp = ret.split_off(ret.find('E').unwrap_or(0));
|
||||||
|
let (exp_sign, exp) = if let Some(stripped) = exp.strip_prefix("E-") {
|
||||||
|
('-', stripped)
|
||||||
|
} else {
|
||||||
|
('+', &exp[1..])
|
||||||
|
};
|
||||||
|
|
||||||
|
let sign = if !ret.starts_with('-') { " " } else { "" };
|
||||||
|
format!("{}{} E{}{:0>pad$}", sign, ret, exp_sign, exp, pad = 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn engineering(f: f64, 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
|
||||||
|
// 1,000 => 1000E3
|
||||||
|
let all = format!(" {:.precision$E}", f, precision = precision)
|
||||||
|
// Remove . since it can be moved
|
||||||
|
.replacen(".", "", 1)
|
||||||
|
// Add 00E before E here so the length is enough for slicing below
|
||||||
|
.replacen("E", "00E", 1);
|
||||||
|
// Extract mantissa and the string representation of the exponent. Unwrap should be safe as formatter will insert E
|
||||||
|
// 1000E3 => (1000, E3)
|
||||||
|
let (num_str, exp_str) = all.split_at(all.find('E').unwrap());
|
||||||
|
// Extract the exponent as an isize. This should always be true because f64 max will be ~400
|
||||||
|
// E3 => 3 as isize
|
||||||
|
let exp = exp_str[1..].parse::<isize>().unwrap();
|
||||||
|
// Sign of the exponent. If string representation starts with E-, then negative
|
||||||
|
let display_exp_sign = if let Some(stripped) = exp_str.strip_prefix("E-") {
|
||||||
|
'-'
|
||||||
|
} else {
|
||||||
|
'+'
|
||||||
|
};
|
||||||
|
|
||||||
|
// The exponent to display. Always a multiple of 3 in engineering mode. Always positive because sign is added with display_exp_sign above
|
||||||
|
// 100 => 0, 1000 => 3, .1 => 3 (but will show as -3)
|
||||||
|
let display_exp = (exp.div_euclid(3) * 3).abs();
|
||||||
|
// Number of whole digits. Always 1, 2, or 3 depending on exponent divisibility
|
||||||
|
let num_whole_digits = exp.rem_euclid(3) as usize + 1;
|
||||||
|
|
||||||
|
// If this is a negative number, strip off the added space, otherwise keep the space (and next digit)
|
||||||
|
let num_str = if num_str.strip_prefix(" -").is_some() {
|
||||||
|
&num_str[1..]
|
||||||
|
} else {
|
||||||
|
num_str
|
||||||
|
};
|
||||||
|
|
||||||
|
// Whole portion of number. Slice is safe because the num_whole_digits is always 3 and the num_str will always have length >= 3 since precision in all=2 (+original whole digit)
|
||||||
|
// Original number is 1,000 => whole will be 1, if original is 0.01, whole will be 10
|
||||||
|
let whole = &num_str[0..(num_whole_digits + 1)];
|
||||||
|
// Decimal portion of the number. Sliced from the number of whole digits to the *requested* precision. Precision generated in all will be requested precision + 2
|
||||||
|
let decimal = &num_str[(num_whole_digits + 1)..(precision + num_whole_digits + 1)];
|
||||||
|
// Right align whole portion, always have decimal point
|
||||||
|
format!(
|
||||||
|
"{: >4}.{} E{}{:0>pad$}",
|
||||||
|
// display_sign,
|
||||||
|
whole,
|
||||||
|
decimal,
|
||||||
|
display_exp_sign,
|
||||||
|
display_exp,
|
||||||
|
pad = 2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn separated(f: f64, sep: char) -> String {
|
||||||
|
let mut ret = f.to_string();
|
||||||
|
let start = if ret.starts_with('-') { 1 } else { 0 };
|
||||||
|
let end = ret.find('.').unwrap_or_else(|| ret.len());
|
||||||
|
for i in 0..((end - start - 1).div_euclid(3)) {
|
||||||
|
ret.insert(end - (i + 1) * 3, sep);
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn test_scientific() {
|
||||||
|
for (f, p, s) in vec![
|
||||||
|
// Basic
|
||||||
|
(1.0, 0, " 1 E+00"),
|
||||||
|
(-1.0, 0, "-1 E+00"),
|
||||||
|
(100.0, 0, " 1 E+02"),
|
||||||
|
(0.1, 0, " 1 E-01"),
|
||||||
|
(0.01, 0, " 1 E-02"),
|
||||||
|
(-0.1, 0, "-1 E-01"),
|
||||||
|
// i
|
||||||
|
(1.0, 0, " 1 E+00"),
|
||||||
|
// Precision
|
||||||
|
(-0.123456789, 3, "-1.235 E-01"),
|
||||||
|
(-0.123456789, 2, "-1.23 E-01"),
|
||||||
|
(-0.123456789, 2, "-1.23 E-01"),
|
||||||
|
(-1e99, 2, "-1.00 E+99"),
|
||||||
|
(-1e100, 2, "-1.00 E+100"),
|
||||||
|
// Rounding
|
||||||
|
(0.5, 2, " 5.00 E-01"),
|
||||||
|
(0.5, 1, " 5.0 E-01"),
|
||||||
|
(0.5, 0, " 5 E-01"),
|
||||||
|
(1.5, 2, " 1.50 E+00"),
|
||||||
|
(1.5, 1, " 1.5 E+00"),
|
||||||
|
(1.5, 0, " 2 E+00"),
|
||||||
|
] {
|
||||||
|
assert_eq!(scientific(f, p), s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_separated() {
|
||||||
|
for (f, c, s) in vec![
|
||||||
|
(100.0, ',', "100"),
|
||||||
|
(100.0, ',', "100"),
|
||||||
|
(-100.0, ',', "-100"),
|
||||||
|
(1_000.0, ',', "1,000"),
|
||||||
|
(-1_000.0, ',', "-1,000"),
|
||||||
|
(10_000.0, ',', "10,000"),
|
||||||
|
(-10_000.0, ',', "-10,000"),
|
||||||
|
(100_000.0, ',', "100,000"),
|
||||||
|
(-100_000.0, ',', "-100,000"),
|
||||||
|
(1_000_000.0, ',', "1,000,000"),
|
||||||
|
(-1_000_000.0, ',', "-1,000,000"),
|
||||||
|
(1_000_000.123456789, ',', "1,000,000.123456789"),
|
||||||
|
(-1_000_000.123456789, ',', "-1,000,000.123456789"),
|
||||||
|
(1_000_000.123456789, ' ', "1 000 000.123456789"),
|
||||||
|
(1_000_000.123456789, ' ', "1 000 000.123456789"),
|
||||||
|
] {
|
||||||
|
assert_eq!(separated(f, c), s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_engineering() {
|
||||||
|
for (f, c, s) in vec![
|
||||||
|
(100.0, 3, " 100.000 E+00"),
|
||||||
|
(100.0, 3, " 100.000 E+00"),
|
||||||
|
(-100.0, 3, "-100.000 E+00"),
|
||||||
|
(100.0, 0, " 100. E+00"),
|
||||||
|
(-100.0, 0, "-100. E+00"),
|
||||||
|
(0.1, 2, " 100.00 E-03"),
|
||||||
|
(0.01, 2, " 10.00 E-03"),
|
||||||
|
(0.001, 2, " 1.00 E-03"),
|
||||||
|
(0.0001, 2, " 100.00 E-06"),
|
||||||
|
// Rounding
|
||||||
|
(0.5, 2, " 500.00 E-03"),
|
||||||
|
(0.5, 1, " 500.0 E-03"),
|
||||||
|
(0.5, 0, " 500. E-03"),
|
||||||
|
(1.5, 2, " 1.50 E+00"),
|
||||||
|
(1.5, 1, " 1.5 E+00"),
|
||||||
|
(1.5, 0, " 2. E+00"),
|
||||||
|
] {
|
||||||
|
assert_eq!(engineering(f, c), s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,2 +0,0 @@
|
|||||||
pub mod calc;
|
|
||||||
pub mod constants;
|
|
@ -1,57 +1,13 @@
|
|||||||
// I am not yet worthy for this yet: clippy::restriction
|
#![allow(unused_variables)]
|
||||||
#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
|
#![allow(dead_code)]
|
||||||
// This is intended behavior
|
|
||||||
#![allow(clippy::cast_possible_truncation)]
|
|
||||||
// Cannot fix this, so don't warn me about it
|
|
||||||
#![allow(clippy::multiple_crate_versions)]
|
|
||||||
|
|
||||||
use rpn_rs::{calc, constants};
|
mod calc;
|
||||||
mod event;
|
mod event;
|
||||||
use tracing::info;
|
mod format;
|
||||||
|
|
||||||
const BORDER_SIZE: u16 = 2;
|
|
||||||
const HELP_TEXT: &str = "\
|
|
||||||
+ => Add s => Sin\n\
|
|
||||||
- => Subtract c => Cos\n\
|
|
||||||
/ => Divide S => ASin\n\
|
|
||||||
n => Negate C => ACos\n\
|
|
||||||
| => Abs T => ATan\n\
|
|
||||||
i => Inverse v => Sqrt\n\
|
|
||||||
% => Modulo u => Undo\n\
|
|
||||||
\\ => Drop U => Redo\n\
|
|
||||||
? => IntegerDivide ^ => Pow\n\
|
|
||||||
<ret> => Dup l => Log\n\
|
|
||||||
L => Ln e => *10^\n\
|
|
||||||
_ => Explode vector or matrix\n\
|
|
||||||
<right> => Swap <down> => Edit\n\
|
|
||||||
uU => Undo/Redo ` => Constants\n\
|
|
||||||
r => Load Register R => Save Register\n\
|
|
||||||
m => Macros @ => Settings\n\
|
|
||||||
^s => Save Config ^l => Load Config\
|
|
||||||
";
|
|
||||||
const SETTINGS_HELP_TEXT: &str = "\
|
|
||||||
d => Degrees\n\
|
|
||||||
r => Radians\n\
|
|
||||||
g => Grads\n\
|
|
||||||
_ => Default\n\
|
|
||||||
, => Comma separated\n\
|
|
||||||
<space> => Space separated\n\
|
|
||||||
s => Scientific\n\
|
|
||||||
S => Scientific (stack precision)\n\
|
|
||||||
e => Engineering\n\
|
|
||||||
E => Engineering (stack precision)\n\
|
|
||||||
f => Fixed\n\
|
|
||||||
F => Fixed (stack precision)\n\
|
|
||||||
w => Do not write settings and stack on quit (default)\n\
|
|
||||||
W => Write stack and settings on quit\n\
|
|
||||||
L => Left align\n\
|
|
||||||
R => Right align\
|
|
||||||
";
|
|
||||||
|
|
||||||
use calc::{
|
use calc::{
|
||||||
entries::CalculatorEntry,
|
constants::{CalculatorDisplayMode, CalculatorState, RegisterState},
|
||||||
errors::CalculatorResult,
|
errors::CalculatorResult,
|
||||||
types::{CalculatorAlignment, CalculatorState, RegisterState},
|
|
||||||
Calculator,
|
Calculator,
|
||||||
};
|
};
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
@ -62,10 +18,10 @@ use crossterm::{
|
|||||||
use event::{Event, Events};
|
use event::{Event, Events};
|
||||||
// use io::stdout;
|
// use io::stdout;
|
||||||
|
|
||||||
use std::{cmp, convert::TryFrom, error::Error, fs::OpenOptions, io, io::Write};
|
use std::{cmp, convert::TryFrom, error::Error, io, io::Write};
|
||||||
use tui::{
|
use tui::{
|
||||||
backend::CrosstermBackend,
|
backend::CrosstermBackend,
|
||||||
layout::{Constraint, Direction, Layout, Rect},
|
layout::{Constraint, Direction, Layout},
|
||||||
style::{Modifier, Style},
|
style::{Modifier, Style},
|
||||||
terminal::Frame,
|
terminal::Frame,
|
||||||
text::{Span, Spans, Text},
|
text::{Span, Spans, Text},
|
||||||
@ -100,7 +56,7 @@ impl Default for App {
|
|||||||
Ok(c) => (c, None),
|
Ok(c) => (c, None),
|
||||||
Err(e) => (Calculator::default(), Some(format!("{}", e))),
|
Err(e) => (Calculator::default(), Some(format!("{}", e))),
|
||||||
};
|
};
|
||||||
Self {
|
App {
|
||||||
calculator,
|
calculator,
|
||||||
error_msg,
|
error_msg,
|
||||||
state: AppState::Calculator,
|
state: AppState::Calculator,
|
||||||
@ -108,33 +64,44 @@ impl Default for App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl App {
|
impl App {
|
||||||
// This function is long because it contains help text
|
fn draw_clippy_dialogs<T: std::io::Write>(&mut self, f: &mut Frame<CrosstermBackend<T>>) {
|
||||||
fn draw_clippy_dialogs<T: Write>(&mut self, f: &mut Frame<CrosstermBackend<T>>) {
|
match (&self.state, self.calculator.get_state()) {
|
||||||
match (&self.state, &self.calculator.state) {
|
|
||||||
(AppState::Help, _) => {
|
(AppState::Help, _) => {
|
||||||
draw_clippy_rect(
|
draw_clippy_rect(
|
||||||
&ClippyRectangle {
|
ClippyRectangle {
|
||||||
title: "Help",
|
title: "Help",
|
||||||
msg: HELP_TEXT,
|
msg: "\
|
||||||
|
+ => Add s => Sin\n\
|
||||||
|
- => Subtract c => Cos\n\
|
||||||
|
* => Multiply t => Tan\n\
|
||||||
|
/ => Divide S => ASin\n\
|
||||||
|
n => Negate C => ACos\n\
|
||||||
|
| => Abs T => ATan\n\
|
||||||
|
i => Inverse v => Sqrt\n\
|
||||||
|
% => Modulo u => Undo\n\
|
||||||
|
\\ => Drop U => Redo\n\
|
||||||
|
? => IntegerDivide ^ => Pow\n\
|
||||||
|
<ret> => Dup l => Log\n\
|
||||||
|
L => Ln e => *10^\n\
|
||||||
|
<right> => Swap <down> => Edit\n\
|
||||||
|
uU => Undo/Redo ` => Constants\n\
|
||||||
|
m => Macros rR => Registers\n\
|
||||||
|
^s => Save Config ^l => Load Config\n\
|
||||||
|
@ => Settings\
|
||||||
|
",
|
||||||
},
|
},
|
||||||
f,
|
f,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
(AppState::Calculator, CalculatorState::WaitingForConstant) => {
|
(AppState::Calculator, CalculatorState::WaitingForConstant) => {
|
||||||
draw_clippy_rect(
|
draw_clippy_rect(
|
||||||
&ClippyRectangle {
|
ClippyRectangle {
|
||||||
title: "Constants",
|
title: "Constants",
|
||||||
msg: self
|
msg: self
|
||||||
.calculator
|
.calculator
|
||||||
.constants
|
.get_constants_iter()
|
||||||
.iter()
|
|
||||||
.map(|(key, constant)| {
|
.map(|(key, constant)| {
|
||||||
format!(
|
format!("{}: {} ({})", key, constant.help, constant.value)
|
||||||
"{}: {} ({})",
|
|
||||||
key,
|
|
||||||
constant.help,
|
|
||||||
constant.value.format_entry(&self.calculator.display_mode)
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.fold(String::new(), |acc, s| acc + &s + "\n")
|
.fold(String::new(), |acc, s| acc + &s + "\n")
|
||||||
.trim_end(),
|
.trim_end(),
|
||||||
@ -148,19 +115,12 @@ impl App {
|
|||||||
RegisterState::Load => "Registers",
|
RegisterState::Load => "Registers",
|
||||||
};
|
};
|
||||||
draw_clippy_rect(
|
draw_clippy_rect(
|
||||||
&ClippyRectangle {
|
ClippyRectangle {
|
||||||
title,
|
title,
|
||||||
msg: self
|
msg: self
|
||||||
.calculator
|
.calculator
|
||||||
.registers
|
.get_registers_iter()
|
||||||
.iter()
|
.map(|(key, value)| format!("{}: {}", key, value))
|
||||||
.map(|(key, value)| {
|
|
||||||
format!(
|
|
||||||
"{}: {}",
|
|
||||||
key,
|
|
||||||
value.format_entry(&self.calculator.display_mode)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.fold(String::new(), |acc, s| acc + &s + "\n")
|
.fold(String::new(), |acc, s| acc + &s + "\n")
|
||||||
.trim_end(),
|
.trim_end(),
|
||||||
},
|
},
|
||||||
@ -169,12 +129,11 @@ impl App {
|
|||||||
}
|
}
|
||||||
(AppState::Calculator, CalculatorState::WaitingForMacro) => {
|
(AppState::Calculator, CalculatorState::WaitingForMacro) => {
|
||||||
draw_clippy_rect(
|
draw_clippy_rect(
|
||||||
&ClippyRectangle {
|
ClippyRectangle {
|
||||||
title: "Macros",
|
title: "Macros",
|
||||||
msg: self
|
msg: self
|
||||||
.calculator
|
.calculator
|
||||||
.macros
|
.get_macros_iter()
|
||||||
.iter()
|
|
||||||
.map(|(key, mac)| format!("{}: {}", key, mac.help))
|
.map(|(key, mac)| format!("{}: {}", key, mac.help))
|
||||||
.fold(String::new(), |acc, s| acc + &s + "\n")
|
.fold(String::new(), |acc, s| acc + &s + "\n")
|
||||||
.trim_end(),
|
.trim_end(),
|
||||||
@ -184,9 +143,23 @@ impl App {
|
|||||||
}
|
}
|
||||||
(AppState::Calculator, CalculatorState::WaitingForSetting) => {
|
(AppState::Calculator, CalculatorState::WaitingForSetting) => {
|
||||||
draw_clippy_rect(
|
draw_clippy_rect(
|
||||||
&ClippyRectangle {
|
ClippyRectangle {
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
msg: SETTINGS_HELP_TEXT,
|
msg: "\
|
||||||
|
d => Degrees\n\
|
||||||
|
r => Radians\n\
|
||||||
|
_ => Default\n\
|
||||||
|
, => Comma separated\n\
|
||||||
|
<space> => Space separated\n\
|
||||||
|
s => Scientific\n\
|
||||||
|
S => Scientific (stack precision)\n\
|
||||||
|
e => Engineering\n\
|
||||||
|
E => Engineering (stack precision)\n\
|
||||||
|
f => Engineering\n\
|
||||||
|
F => Engineering (stack precision)\n\
|
||||||
|
w => Do not write settings and stack on quit (default)\n\
|
||||||
|
W => Write stack and settings on quit\
|
||||||
|
",
|
||||||
},
|
},
|
||||||
f,
|
f,
|
||||||
);
|
);
|
||||||
@ -195,7 +168,21 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_status_line<T: Write>(&mut self, f: &mut Frame<CrosstermBackend<T>>, chunk: Rect) {
|
pub fn terminal_draw_func<T: std::io::Write>(&mut self, f: &mut Frame<CrosstermBackend<T>>) {
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.margin(2)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Length(1),
|
||||||
|
Constraint::Min(1),
|
||||||
|
Constraint::Length(3),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(f.size());
|
||||||
|
|
||||||
|
// Help message
|
||||||
let msg = match (&self.error_msg, &self.state) {
|
let msg = match (&self.error_msg, &self.state) {
|
||||||
(Some(e), _) => vec![
|
(Some(e), _) => vec![
|
||||||
Span::raw("Error: "),
|
Span::raw("Error: "),
|
||||||
@ -207,7 +194,16 @@ impl App {
|
|||||||
Span::styled("q", Style::default().add_modifier(Modifier::BOLD)),
|
Span::styled("q", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
Span::raw(" to exit, "),
|
Span::raw(" to exit, "),
|
||||||
Span::styled("h", Style::default().add_modifier(Modifier::BOLD)),
|
Span::styled("h", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
Span::raw(format!(" for help - {}", self.calculator.get_status_line())),
|
Span::raw(format!(
|
||||||
|
" for help - [{}] [{}] [{}]",
|
||||||
|
self.calculator.get_display_mode(),
|
||||||
|
self.calculator.get_angle_mode(),
|
||||||
|
if self.calculator.get_save_on_close() {
|
||||||
|
"W"
|
||||||
|
} else {
|
||||||
|
"w"
|
||||||
|
}
|
||||||
|
)),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
(None, _) => vec![
|
(None, _) => vec![
|
||||||
@ -218,127 +214,66 @@ impl App {
|
|||||||
};
|
};
|
||||||
let text = Text::from(Spans::from(msg));
|
let text = Text::from(Spans::from(msg));
|
||||||
let help_message = Paragraph::new(text);
|
let help_message = Paragraph::new(text);
|
||||||
f.render_widget(help_message, chunk);
|
f.render_widget(help_message, chunks[0]);
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_input_box<T: Write>(&mut self, f: &mut Frame<CrosstermBackend<T>>, chunk: Rect) {
|
// Stack
|
||||||
let input = Paragraph::new(self.calculator.get_l().as_ref())
|
|
||||||
.style(Style::default())
|
|
||||||
.block(Block::default().borders(Borders::ALL).title("Input"));
|
|
||||||
f.render_widget(input, chunk);
|
|
||||||
|
|
||||||
f.set_cursor(
|
|
||||||
chunk.x + self.calculator.get_l().len() as u16 + 1,
|
|
||||||
chunk.y + 1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_stack<T: Write>(&mut self, f: &mut Frame<CrosstermBackend<T>>, chunk: Rect) {
|
|
||||||
let mut stack: Vec<ListItem> = self
|
let mut stack: Vec<ListItem> = self
|
||||||
.calculator
|
.calculator
|
||||||
.stack
|
.get_stack()
|
||||||
.iter()
|
.iter()
|
||||||
.take(chunk.height.saturating_sub(BORDER_SIZE) as usize)
|
.take(chunks[1].height as usize - 2)
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.rev()
|
.rev()
|
||||||
.map(|(i, m)| {
|
.map(|(i, m)| {
|
||||||
let number = m.format_entry(&self.calculator.display_mode);
|
let content = vec![Spans::from(Span::raw(
|
||||||
let content = match self.calculator.calculator_alignment {
|
match self.calculator.get_display_mode() {
|
||||||
CalculatorAlignment::Left => {
|
CalculatorDisplayMode::Default => format!("{:>2}: {}", i, *m),
|
||||||
format!("{:>2}: {}", i, number)
|
CalculatorDisplayMode::Separated { separator } => {
|
||||||
|
format!("{:>2}: {}", i, format::separated(*m, *separator))
|
||||||
}
|
}
|
||||||
CalculatorAlignment::Right => {
|
CalculatorDisplayMode::Scientific { precision } => {
|
||||||
let ret = format!("{} :{:>2}", number, i);
|
format!("{:>2}: {}", i, format::scientific(*m, *precision))
|
||||||
if ret.len() < chunk.width.saturating_sub(BORDER_SIZE) as usize {
|
|
||||||
format!(
|
|
||||||
"{: >width$}",
|
|
||||||
ret,
|
|
||||||
width = chunk.width.saturating_sub(BORDER_SIZE) as usize
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
ret
|
|
||||||
}
|
}
|
||||||
|
CalculatorDisplayMode::Engineering { precision } => {
|
||||||
|
format!("{:>2}: {}", i, format::engineering(*m, *precision))
|
||||||
}
|
}
|
||||||
};
|
CalculatorDisplayMode::Fixed { precision } => {
|
||||||
ListItem::new(Spans::from(Span::raw(content)))
|
format!("{:>2}: {:.precision$}", i, m, precision = precision)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))];
|
||||||
|
ListItem::new(content)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
for _ in 0..(chunk
|
for _ in 0..(chunks[1]
|
||||||
.height
|
.height
|
||||||
.saturating_sub(stack.len() as u16)
|
.saturating_sub(stack.len() as u16)
|
||||||
.saturating_sub(BORDER_SIZE))
|
.saturating_sub(2))
|
||||||
{
|
{
|
||||||
stack.insert(
|
stack.insert(0, ListItem::new(Span::raw("~")));
|
||||||
0,
|
|
||||||
ListItem::new(Span::raw(match self.calculator.calculator_alignment {
|
|
||||||
CalculatorAlignment::Left => String::from("~"),
|
|
||||||
CalculatorAlignment::Right => format!(
|
|
||||||
"{: >width$}",
|
|
||||||
"~",
|
|
||||||
width = chunk.width.saturating_sub(BORDER_SIZE) as usize
|
|
||||||
),
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let stack = List::new(stack).block(Block::default().borders(Borders::ALL).title("Stack"));
|
let stack = List::new(stack).block(Block::default().borders(Borders::ALL).title("Stack"));
|
||||||
f.render_widget(stack, chunk);
|
f.render_widget(stack, chunks[1]);
|
||||||
}
|
|
||||||
|
|
||||||
pub fn terminal_draw_func<T: Write>(&mut self, f: &mut Frame<CrosstermBackend<T>>) {
|
// Input box
|
||||||
let chunks = Layout::default()
|
let input = Paragraph::new(self.calculator.get_l().as_ref())
|
||||||
.direction(Direction::Vertical)
|
.style(Style::default())
|
||||||
.margin(BORDER_SIZE)
|
.block(Block::default().borders(Borders::ALL).title("Input"));
|
||||||
.constraints(
|
f.render_widget(input, chunks[2]);
|
||||||
[
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Min(1),
|
|
||||||
Constraint::Length(3),
|
|
||||||
]
|
|
||||||
.as_ref(),
|
|
||||||
)
|
|
||||||
.split(f.size());
|
|
||||||
|
|
||||||
self.draw_stack(f, chunks[1]);
|
f.set_cursor(
|
||||||
self.draw_input_box(f, chunks[2]);
|
chunks[2].x + self.calculator.get_l().len() as u16 + 1,
|
||||||
// Draw the status line last in case something above has an error
|
chunks[2].y + 1,
|
||||||
self.draw_status_line(f, chunks[0]);
|
);
|
||||||
|
|
||||||
|
// Help text
|
||||||
self.draw_clippy_dialogs(f);
|
self.draw_clippy_dialogs(f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
@ -363,7 +298,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
Err(e) => Some(format!("{}", e)),
|
Err(e) => Some(format!("{}", e)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Event::Tick => continue,
|
_ => continue,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -377,10 +312,8 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
app.calculator.close().map_err(|e| e.into())
|
app.calculator.close().map_err(|e| e.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function is long since it maps keys to their proper value
|
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
fn handle_key(app: &mut App, key: KeyEvent) -> CalculatorResult<CalculatorResponse> {
|
fn handle_key(app: &mut App, key: KeyEvent) -> CalculatorResult<CalculatorResponse> {
|
||||||
match (&app.state, &app.calculator.state) {
|
match (&app.state, app.calculator.get_state()) {
|
||||||
(AppState::Calculator, CalculatorState::Normal) => match key {
|
(AppState::Calculator, CalculatorState::Normal) => match key {
|
||||||
KeyEvent {
|
KeyEvent {
|
||||||
code: KeyCode::Char('q'),
|
code: KeyCode::Char('q'),
|
||||||
@ -410,10 +343,6 @@ 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,
|
||||||
@ -452,7 +381,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 {
|
||||||
@ -485,14 +414,6 @@ fn handle_key(app: &mut App, key: KeyEvent) -> CalculatorResult<CalculatorRespon
|
|||||||
} => {
|
} => {
|
||||||
app.calculator.take_input(c)?;
|
app.calculator.take_input(c)?;
|
||||||
}
|
}
|
||||||
KeyEvent {
|
|
||||||
code: KeyCode::Char(c),
|
|
||||||
modifiers: KeyModifiers::SHIFT,
|
|
||||||
} => {
|
|
||||||
for c in c.to_uppercase() {
|
|
||||||
app.calculator.take_input(c)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -505,7 +426,7 @@ struct ClippyRectangle<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ClippyRectangle<'_> {
|
impl ClippyRectangle<'_> {
|
||||||
// Cannot be static since the clippy rectangle's text can change
|
// TODO: Make this static somehow
|
||||||
fn size(&self) -> Dimensions {
|
fn size(&self) -> Dimensions {
|
||||||
let (width, height) = self.msg.lines().fold((0, 0), |(width, height), l| {
|
let (width, height) = self.msg.lines().fold((0, 0), |(width, height), l| {
|
||||||
(cmp::max(width, l.len()), height + 1)
|
(cmp::max(width, l.len()), height + 1)
|
||||||
@ -517,15 +438,15 @@ impl ClippyRectangle<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_clippy_rect<T: Write>(c: &ClippyRectangle, f: &mut Frame<CrosstermBackend<T>>) {
|
fn draw_clippy_rect<T: std::io::Write>(c: ClippyRectangle, f: &mut Frame<CrosstermBackend<T>>) {
|
||||||
|
let block = Block::default().title(c.title).borders(Borders::ALL);
|
||||||
let dimensions = c.size();
|
let dimensions = c.size();
|
||||||
let popup_layout = Layout::default()
|
let popup_layout = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints(
|
.constraints(
|
||||||
[
|
[
|
||||||
// Padding
|
|
||||||
Constraint::Min(1),
|
Constraint::Min(1),
|
||||||
Constraint::Length(dimensions.height + BORDER_SIZE),
|
Constraint::Length(dimensions.height + 2),
|
||||||
]
|
]
|
||||||
.as_ref(),
|
.as_ref(),
|
||||||
)
|
)
|
||||||
@ -534,9 +455,8 @@ fn draw_clippy_rect<T: Write>(c: &ClippyRectangle, f: &mut Frame<CrosstermBacken
|
|||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints(
|
.constraints(
|
||||||
[
|
[
|
||||||
// Padding
|
|
||||||
Constraint::Min(1),
|
Constraint::Min(1),
|
||||||
Constraint::Length(cmp::max(dimensions.width, c.title.len() as u16) + BORDER_SIZE),
|
Constraint::Length(cmp::max(dimensions.width, c.title.len() as u16) + 2),
|
||||||
]
|
]
|
||||||
.as_ref(),
|
.as_ref(),
|
||||||
)
|
)
|
Loading…
Reference in New Issue
Block a user