This commit is contained in:
2026-08-01 22:41:59 +03:00
parent 7d050f3eb2
commit 4f043fc1c0
12 changed files with 1284 additions and 49 deletions
+4
View File
@@ -1,2 +1,6 @@
[target.x86_64-pc-windows-gnu]
linker = "x86_64-w64-mingw32-gcc"
rustflags = [
"-C", "link-arg=-static-libgcc",
"-C", "link-arg=-static-libstdc++",
]
+1
View File
@@ -5,3 +5,4 @@
/dist/rcalc.AppDir/
/dist/.glibc-build.txt
.rcalc_last_bump
/target-windows
Generated
+1 -1
View File
@@ -3904,7 +3904,7 @@ dependencies = [
[[package]]
name = "rcalc"
version = "0.1.4"
version = "0.1.11"
dependencies = [
"slint",
"slint-build",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rcalc"
version = "0.1.5"
version = "0.1.12"
edition = "2021"
build = "build.rs"
Vendored Executable
BIN
View File
Binary file not shown.
Vendored
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+861
View File
@@ -0,0 +1,861 @@
//! Engineering (scientific) calculator — formula entry with trig/log/powers.
use std::f64::consts::{E, PI};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AngleMode {
Deg,
Rad,
}
impl AngleMode {
pub fn label(self) -> &'static str {
match self {
AngleMode::Deg => "DEG",
AngleMode::Rad => "RAD",
}
}
pub fn cycle(self) -> Self {
match self {
AngleMode::Deg => AngleMode::Rad,
AngleMode::Rad => AngleMode::Deg,
}
}
fn to_rad(self, x: f64) -> f64 {
match self {
AngleMode::Deg => x.to_radians(),
AngleMode::Rad => x,
}
}
fn from_rad(self, x: f64) -> f64 {
match self {
AngleMode::Deg => x.to_degrees(),
AngleMode::Rad => x,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SciError {
DivByZero,
Domain,
Overflow,
Invalid,
}
impl SciError {
pub fn message(self) -> &'static str {
match self {
SciError::DivByZero => "Cannot divide by zero",
SciError::Domain => "Invalid input",
SciError::Overflow => "Overflow",
SciError::Invalid => "Invalid input",
}
}
}
#[derive(Debug, Clone)]
pub struct Engineering {
entry: String,
typing: bool,
formula: String,
expression: String,
done: bool,
error: Option<SciError>,
angle: AngleMode,
/// 2nd function layer (asin instead of sin, …).
second: bool,
memory: f64,
memory_set: bool,
}
impl Default for Engineering {
fn default() -> Self {
Self::new()
}
}
impl Engineering {
pub fn new() -> Self {
Self {
entry: "0".into(),
typing: false,
formula: String::new(),
expression: String::new(),
done: false,
error: None,
angle: AngleMode::Deg,
second: false,
memory: 0.0,
memory_set: false,
}
}
pub fn display(&self) -> &str {
if let Some(err) = self.error {
return err.message();
}
&self.entry
}
pub fn expression(&self) -> &str {
&self.expression
}
pub fn angle_mode(&self) -> AngleMode {
self.angle
}
pub fn second(&self) -> bool {
self.second
}
pub fn has_memory(&self) -> bool {
self.memory_set
}
pub fn cycle_angle(&mut self) {
self.angle = self.angle.cycle();
}
pub fn toggle_second(&mut self) {
self.second = !self.second;
}
pub fn clear_all(&mut self) {
let angle = self.angle;
let mem = (self.memory, self.memory_set);
*self = Self::new();
self.angle = angle;
self.memory = mem.0;
self.memory_set = mem.1;
}
pub fn clear_entry(&mut self) {
self.error = None;
self.entry = "0".into();
self.typing = false;
self.sync_expr();
}
pub fn backspace(&mut self) {
if self.error.is_some() {
self.clear_entry();
return;
}
if self.done {
return;
}
if self.typing {
self.entry.pop();
if self.entry.is_empty() || self.entry == "-" {
self.entry = "0".into();
self.typing = false;
}
self.sync_expr();
return;
}
let mut s = self.formula.trim_end().to_string();
if s.ends_with('(') {
s.pop();
while s
.chars()
.last()
.is_some_and(|c| c.is_ascii_alphanumeric() || c == '_')
{
s.pop();
}
} else if s.chars().last().is_some_and(|c| "+−×÷^)".contains(c)) {
s.pop();
} else {
// pop trailing number / constant
while s
.chars()
.last()
.is_some_and(|c| c.is_ascii_digit() || c == '.' || c == 'π' || c == 'e')
{
s.pop();
}
}
while s.ends_with(' ') {
s.pop();
}
self.formula = s;
self.sync_expr();
}
pub fn input_digit(&mut self, d: char) {
if self.error.is_some() {
self.clear_all();
}
self.begin_entry_if_done();
if !self.typing {
self.entry = d.to_string();
self.typing = true;
} else if self.entry == "0" {
self.entry = d.to_string();
} else if self.entry == "-0" {
self.entry = format!("-{d}");
} else if digit_count(&self.entry) < 16 {
self.entry.push(d);
}
self.sync_expr();
}
pub fn input_dot(&mut self) {
if self.error.is_some() {
self.clear_all();
}
self.begin_entry_if_done();
if !self.typing {
self.entry = "0.".into();
self.typing = true;
} else if !self.entry.contains('.') {
self.entry.push('.');
}
self.sync_expr();
}
pub fn negate(&mut self) {
if self.error.is_some() {
return;
}
if self.done {
if let Ok(v) = self.entry.parse::<f64>() {
self.set_result(-v);
self.done = true;
}
return;
}
if self.typing || self.entry != "0" {
if let Some(stripped) = self.entry.strip_prefix('-') {
self.entry = stripped.to_string();
} else if self.entry != "0" {
self.entry = format!("-{}", self.entry);
}
self.typing = true;
self.sync_expr();
}
}
pub fn set_op(&mut self, op: &str) {
if self.error.is_some() {
return;
}
if self.done {
self.formula.clear();
self.done = false;
self.typing = true;
}
self.flush_entry();
// Replace trailing binary op.
let t = self.formula.trim_end();
if t.chars().last().is_some_and(|c| "+−×÷^".contains(c)) {
while self.formula.ends_with(' ') {
self.formula.pop();
}
self.formula.pop();
while self.formula.ends_with(' ') {
self.formula.pop();
}
}
if !self.formula.is_empty() {
self.formula.push(' ');
}
self.formula.push_str(op);
self.formula.push(' ');
self.entry = "0".into();
self.typing = false;
self.second = false;
self.sync_expr();
}
pub fn paren_open(&mut self) {
if self.error.is_some() {
return;
}
if self.done {
self.formula.clear();
self.done = false;
self.entry = "0".into();
self.typing = false;
}
if self.typing {
self.flush_entry();
let t = self.formula.trim_end();
if t.chars()
.last()
.is_some_and(|c| c.is_ascii_digit() || c == ')' || c == 'π' || c == 'e')
{
self.formula.push_str(" × ");
}
} else {
let t = self.formula.trim_end();
if t.chars()
.last()
.is_some_and(|c| c.is_ascii_digit() || c == ')')
{
self.formula.push_str(" × ");
}
}
self.formula.push('(');
self.entry = "0".into();
self.typing = false;
self.sync_expr();
}
pub fn paren_close(&mut self) {
if self.error.is_some() || self.done {
return;
}
let opens = self.formula.chars().filter(|&c| c == '(').count();
let closes = self.formula.chars().filter(|&c| c == ')').count();
if opens <= closes {
return;
}
if self.typing || (!self.formula.ends_with('(') && self.entry != "0") {
self.flush_entry();
}
self.formula.push(')');
self.entry = "0".into();
self.typing = false;
self.sync_expr();
}
/// Insert a function prefix like `sin(` or constant `π`.
pub fn insert_func(&mut self, name: &str) {
if self.error.is_some() {
self.clear_all();
}
if self.done {
self.formula.clear();
self.done = false;
self.entry = "0".into();
self.typing = false;
}
if self.typing {
self.flush_entry();
let t = self.formula.trim_end();
if t.chars()
.last()
.is_some_and(|c| c.is_ascii_digit() || c == ')' || c == 'π' || c == 'e')
{
self.formula.push_str(" × ");
}
} else {
let t = self.formula.trim_end();
if t.chars()
.last()
.is_some_and(|c| c.is_ascii_digit() || c == ')' || c == 'π' || c == 'e')
{
self.formula.push_str(" × ");
}
}
self.formula.push_str(name);
self.formula.push('(');
self.entry = "0".into();
self.typing = false;
self.second = false;
self.sync_expr();
}
pub fn insert_const(&mut self, name: &str) {
if self.error.is_some() {
self.clear_all();
}
if self.done {
self.formula.clear();
self.done = false;
}
if self.typing {
self.flush_entry();
self.formula.push_str(" × ");
} else {
let t = self.formula.trim_end();
if t.chars()
.last()
.is_some_and(|c| c.is_ascii_digit() || c == ')' || c == 'π' || c == 'e')
{
self.formula.push_str(" × ");
}
}
self.formula.push_str(name);
self.entry = "0".into();
self.typing = false;
self.second = false;
self.sync_expr();
}
pub fn equals(&mut self) {
if self.error.is_some() {
return;
}
let mut src = self.formula.clone();
let ends_paren = src.trim_end().ends_with(')');
if !ends_paren {
if !src.is_empty() && !src.ends_with(' ') && !src.ends_with('(') {
src.push(' ');
}
// If formula ends with func( and entry is being typed, append entry then )
if src.ends_with('(') || self.typing || src.is_empty() {
src.push_str(&self.entry);
} else if !src
.trim_end()
.chars()
.last()
.is_some_and(|c| c.is_ascii_digit() || c == 'π' || c == 'e' || c == ')')
{
src.push_str(&self.entry);
}
} else if src.is_empty() {
src = self.entry.clone();
}
let opens = src.chars().filter(|&c| c == '(').count();
let closes = src.chars().filter(|&c| c == ')').count();
for _ in 0..opens.saturating_sub(closes) {
src.push(')');
}
match eval_sci(&src, self.angle) {
Ok(v) => {
self.expression = format!("{src} =");
self.set_result(v);
self.formula.clear();
self.typing = false;
self.done = true;
self.second = false;
}
Err(e) => self.fail(e),
}
}
pub fn memory_clear(&mut self) {
self.memory = 0.0;
self.memory_set = false;
}
pub fn memory_recall(&mut self) {
if self.error.is_some() {
self.clear_all();
}
if self.done {
self.formula.clear();
self.done = false;
}
self.entry = format_num(self.memory);
self.typing = true;
self.sync_expr();
}
pub fn memory_add(&mut self) {
if let Ok(v) = self.current() {
self.memory += v;
self.memory_set = true;
}
}
pub fn memory_sub(&mut self) {
if let Ok(v) = self.current() {
self.memory -= v;
self.memory_set = true;
}
}
pub fn memory_store(&mut self) {
if let Ok(v) = self.current() {
self.memory = v;
self.memory_set = true;
}
}
fn current(&self) -> Result<f64, SciError> {
self.entry.parse().map_err(|_| SciError::Invalid)
}
fn begin_entry_if_done(&mut self) {
if self.done {
self.formula.clear();
self.expression.clear();
self.done = false;
}
}
fn flush_entry(&mut self) {
if !self.typing && self.entry == "0" && self.formula.trim_end().ends_with(')') {
return;
}
if !self.typing && self.entry == "0" && self.formula.trim_end().ends_with('π') {
return;
}
if !self.typing && self.entry == "0" && self.formula.trim_end().ends_with('e') {
// constant e already there
let t = self.formula.trim_end();
if t.ends_with('e')
&& t.len() > 1
&& !t.chars().nth_back(1).unwrap_or(' ').is_ascii_alphanumeric()
{
return;
}
if t == "e" || t.ends_with(" e") {
return;
}
}
if !self.formula.is_empty()
&& !self.formula.ends_with(' ')
&& !self.formula.ends_with('(')
{
self.formula.push(' ');
}
self.formula.push_str(&self.entry);
self.typing = false;
}
fn sync_expr(&mut self) {
if self.done {
return;
}
let mut s = self.formula.clone();
if self.typing {
if !s.is_empty() && !s.ends_with(' ') && !s.ends_with('(') {
s.push(' ');
}
s.push_str(&self.entry);
}
self.expression = s;
}
fn set_result(&mut self, v: f64) {
if !v.is_finite() {
self.fail(SciError::Overflow);
return;
}
self.entry = format_num(v);
self.error = None;
}
fn fail(&mut self, e: SciError) {
self.error = Some(e);
self.formula.clear();
self.expression.clear();
self.typing = false;
self.done = false;
}
}
fn digit_count(s: &str) -> usize {
s.chars().filter(|c| c.is_ascii_digit()).count()
}
fn format_num(v: f64) -> String {
if !v.is_finite() {
return "0".into();
}
if (v - v.round()).abs() < 1e-10 && v.abs() < 1e15 {
return format!("{}", v.round() as i64);
}
let s = format!("{:.12}", v);
let s = s.trim_end_matches('0').trim_end_matches('.').to_string();
if s.is_empty() || s == "-" {
"0".into()
} else {
s
}
}
// --- expression evaluator ---
#[derive(Clone, Debug)]
enum Tok {
Num(f64),
Op(char), // + × ÷ ^
LParen,
RParen,
Ident(String),
}
fn eval_sci(src: &str, angle: AngleMode) -> Result<f64, SciError> {
let tokens = tokenize(src)?;
let mut idx = 0;
let v = parse_expr(&tokens, &mut idx, angle)?;
if idx != tokens.len() {
return Err(SciError::Invalid);
}
if !v.is_finite() {
return Err(SciError::Overflow);
}
Ok(v)
}
fn tokenize(src: &str) -> Result<Vec<Tok>, SciError> {
let chars: Vec<char> = src.chars().collect();
let mut i = 0;
let mut out = Vec::new();
while i < chars.len() {
let c = chars[i];
if c.is_whitespace() {
i += 1;
continue;
}
match c {
'+' => {
out.push(Tok::Op('+'));
i += 1;
}
'' | '-' => {
out.push(Tok::Op(''));
i += 1;
}
'×' | '*' => {
out.push(Tok::Op('×'));
i += 1;
}
'÷' | '/' => {
out.push(Tok::Op('÷'));
i += 1;
}
'^' => {
out.push(Tok::Op('^'));
i += 1;
}
'(' => {
out.push(Tok::LParen);
i += 1;
}
')' => {
out.push(Tok::RParen);
i += 1;
}
'π' => {
out.push(Tok::Num(PI));
i += 1;
}
'0'..='9' | '.' => {
let start = i;
i += 1;
while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
i += 1;
}
let lit: String = chars[start..i].iter().collect();
let v: f64 = lit.parse().map_err(|_| SciError::Invalid)?;
out.push(Tok::Num(v));
}
'a'..='z' | 'A'..='Z' | '_' => {
let start = i;
i += 1;
while i < chars.len() && (chars[i].is_ascii_alphanumeric() || chars[i] == '_') {
i += 1;
}
let name: String = chars[start..i].iter().collect();
if name == "e" || name == "E" {
out.push(Tok::Num(E));
} else if name == "pi" || name == "PI" {
out.push(Tok::Num(PI));
} else {
out.push(Tok::Ident(name.to_ascii_lowercase()));
}
}
_ => return Err(SciError::Invalid),
}
}
Ok(out)
}
fn parse_expr(tokens: &[Tok], idx: &mut usize, angle: AngleMode) -> Result<f64, SciError> {
let mut left = parse_term(tokens, idx, angle)?;
while let Some(Tok::Op(op @ ('+' | ''))) = tokens.get(*idx).cloned() {
*idx += 1;
let right = parse_term(tokens, idx, angle)?;
left = if op == '+' { left + right } else { left - right };
}
Ok(left)
}
fn parse_term(tokens: &[Tok], idx: &mut usize, angle: AngleMode) -> Result<f64, SciError> {
let mut left = parse_power(tokens, idx, angle)?;
while let Some(Tok::Op(op @ ('×' | '÷'))) = tokens.get(*idx).cloned() {
*idx += 1;
let right = parse_power(tokens, idx, angle)?;
left = if op == '×' {
left * right
} else {
if right == 0.0 {
return Err(SciError::DivByZero);
}
left / right
};
}
Ok(left)
}
fn parse_power(tokens: &[Tok], idx: &mut usize, angle: AngleMode) -> Result<f64, SciError> {
let base = parse_unary(tokens, idx, angle)?;
if let Some(Tok::Op('^')) = tokens.get(*idx).cloned() {
*idx += 1;
// right-associative
let exp = parse_power(tokens, idx, angle)?;
let r = base.powf(exp);
if !r.is_finite() {
return Err(SciError::Overflow);
}
Ok(r)
} else {
Ok(base)
}
}
fn parse_unary(tokens: &[Tok], idx: &mut usize, angle: AngleMode) -> Result<f64, SciError> {
if let Some(Tok::Op('')) = tokens.get(*idx).cloned() {
*idx += 1;
return Ok(-parse_unary(tokens, idx, angle)?);
}
if let Some(Tok::Op('+')) = tokens.get(*idx).cloned() {
*idx += 1;
return parse_unary(tokens, idx, angle);
}
if let Some(Tok::Ident(name)) = tokens.get(*idx).cloned() {
*idx += 1;
// function call: name (
match tokens.get(*idx) {
Some(Tok::LParen) => {
*idx += 1;
let arg = parse_expr(tokens, idx, angle)?;
match tokens.get(*idx) {
Some(Tok::RParen) => *idx += 1,
_ => return Err(SciError::Invalid),
}
apply_func(&name, arg, angle)
}
_ => Err(SciError::Invalid),
}
} else {
parse_primary(tokens, idx, angle)
}
}
fn parse_primary(tokens: &[Tok], idx: &mut usize, angle: AngleMode) -> Result<f64, SciError> {
match tokens.get(*idx).cloned() {
Some(Tok::Num(v)) => {
*idx += 1;
Ok(v)
}
Some(Tok::LParen) => {
*idx += 1;
let v = parse_expr(tokens, idx, angle)?;
match tokens.get(*idx) {
Some(Tok::RParen) => {
*idx += 1;
Ok(v)
}
_ => Err(SciError::Invalid),
}
}
_ => Err(SciError::Invalid),
}
}
fn apply_func(name: &str, x: f64, angle: AngleMode) -> Result<f64, SciError> {
let r = match name {
"sin" => angle.to_rad(x).sin(),
"cos" => angle.to_rad(x).cos(),
"tan" => angle.to_rad(x).tan(),
"asin" => {
if !(-1.0..=1.0).contains(&x) {
return Err(SciError::Domain);
}
angle.from_rad(x.asin())
}
"acos" => {
if !(-1.0..=1.0).contains(&x) {
return Err(SciError::Domain);
}
angle.from_rad(x.acos())
}
"atan" => angle.from_rad(x.atan()),
"sinh" => x.sinh(),
"cosh" => x.cosh(),
"tanh" => x.tanh(),
"ln" => {
if x <= 0.0 {
return Err(SciError::Domain);
}
x.ln()
}
"log" | "log10" => {
if x <= 0.0 {
return Err(SciError::Domain);
}
x.log10()
}
"exp" => x.exp(),
"sqrt" => {
if x < 0.0 {
return Err(SciError::Domain);
}
x.sqrt()
}
"cbrt" => x.cbrt(),
"sq" | "sqr" => x * x,
"cube" => x * x * x,
"inv" => {
if x == 0.0 {
return Err(SciError::DivByZero);
}
1.0 / x
}
"abs" => x.abs(),
"fact" | "factorial" => factorial(x)?,
"tenpow" | "pow10" => 10f64.powf(x),
_ => return Err(SciError::Invalid),
};
if !r.is_finite() {
Err(SciError::Overflow)
} else {
Ok(r)
}
}
fn factorial(x: f64) -> Result<f64, SciError> {
if x < 0.0 || x != x.floor() || x > 170.0 {
return Err(SciError::Domain);
}
let n = x as u32;
let mut r = 1.0;
for i in 2..=n {
r *= f64::from(i);
}
if !r.is_finite() {
Err(SciError::Overflow)
} else {
Ok(r)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sin_90_deg() {
let mut e = Engineering::new();
e.insert_func("sin");
e.input_digit('9');
e.input_digit('0');
e.equals();
assert_eq!(e.display(), "1");
}
#[test]
fn power() {
let mut e = Engineering::new();
e.input_digit('2');
e.set_op("^");
e.input_digit('1');
e.input_digit('0');
e.equals();
assert_eq!(e.display(), "1024");
}
}
+160 -9
View File
@@ -4,6 +4,7 @@
mod convert;
mod crc;
mod engine;
mod engineering;
mod programmer;
use std::cell::RefCell;
@@ -12,6 +13,7 @@ use std::rc::Rc;
use convert::{ConvField, ConvPanel, Converter, Endian, RatioWidth};
use crc::CrcTool;
use engine::{Calculator, Op, StdPanel};
use engineering::Engineering;
use programmer::{Base, ProgOp, Programmer};
use slint::{ComponentHandle, ModelRc, VecModel};
@@ -20,6 +22,7 @@ slint::include_modules!();
#[derive(Clone, Copy, PartialEq, Eq)]
enum Mode {
Standard,
Engineering,
Programmer,
Convert,
Crc,
@@ -28,6 +31,7 @@ enum Mode {
struct State {
mode: Mode,
std: Calculator,
eng: Engineering,
prog: Programmer,
conv: Converter,
crc: CrcTool,
@@ -40,6 +44,7 @@ impl Default for State {
Self {
mode: Mode::Standard,
std: Calculator::new(),
eng: Engineering::new(),
prog: Programmer::new(),
conv: Converter::new(),
crc: CrcTool::new(),
@@ -49,12 +54,34 @@ impl Default for State {
}
}
fn main() -> Result<(), slint::PlatformError> {
fn main() {
install_panic_hook();
#[cfg(windows)]
{
// OpenGL/femtovg often fails on remote desktop / old drivers; software is portable.
if std::env::var_os("SLINT_BACKEND").is_none() {
// SAFETY: single-threaded init before any other threads.
unsafe { std::env::set_var("SLINT_BACKEND", "winit-software") };
}
}
if let Err(e) = run_app() {
let msg = format!("rcalc failed to start:\n{e}\n\nTip: try setting SLINT_BACKEND=winit-software");
log_startup_error(&msg);
#[cfg(windows)]
windows_error_dialog(&msg);
eprintln!("{msg}");
std::process::exit(1);
}
}
fn run_app() -> Result<(), slint::PlatformError> {
// Platform must exist first — calling set_xdg_app_id before AppWindow::new
// returns NoPlatform and the Wayland app_id stays unset (KDE shows the "W" icon).
let ui = AppWindow::new()?;
// Must match the .desktop basename (rcalc.desktop → "rcalc"), before show/run.
slint::set_xdg_app_id("rcalc")?;
let _ = slint::set_xdg_app_id("rcalc");
ui.set_app_title(format!("Rcalc-{}", env!("APP_VERSION")).into());
let state = Rc::new(RefCell::new(State::default()));
@@ -84,6 +111,56 @@ fn main() -> Result<(), slint::PlatformError> {
ui.run()
}
fn install_panic_hook() {
let default = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let msg = format!("rcalc panic:\n{info}");
log_startup_error(&msg);
#[cfg(windows)]
windows_error_dialog(&msg);
default(info);
}));
}
fn log_startup_error(msg: &str) {
let path = std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|d| d.join("rcalc-crash.log")))
.unwrap_or_else(|| std::path::PathBuf::from("rcalc-crash.log"));
let _ = std::fs::write(&path, msg);
}
#[cfg(windows)]
fn windows_error_dialog(msg: &str) {
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
fn wide(s: &str) -> Vec<u16> {
OsStr::new(s).encode_wide().chain(std::iter::once(0)).collect()
}
#[link(name = "user32")]
unsafe extern "system" {
fn MessageBoxW(
hwnd: *mut core::ffi::c_void,
text: *const u16,
caption: *const u16,
flags: u32,
) -> i32;
}
let text = wide(msg);
let caption = wide("Rcalc");
unsafe {
MessageBoxW(
std::ptr::null_mut(),
text.as_ptr(),
caption.as_ptr(),
0x10, // MB_ICONERROR
);
}
}
fn bit_row_model(row: &[(bool, u32)]) -> ModelRc<BitInfo> {
let items: Vec<BitInfo> = row
.iter()
@@ -98,9 +175,10 @@ fn bit_row_model(row: &[(bool, u32)]) -> ModelRc<BitInfo> {
fn window_profile(state: &State) -> u32 {
let mode = match state.mode {
Mode::Standard => 0,
Mode::Programmer => 1,
Mode::Convert => 2,
Mode::Crc => 3,
Mode::Engineering => 1,
Mode::Programmer => 2,
Mode::Convert => 3,
Mode::Crc => 4,
};
let bits = if state.show_bits {
state.prog.word_size().bits()
@@ -123,7 +201,8 @@ fn fit_window(ui: &AppWindow, state: &mut State) {
state.win_profile = profile;
let (w, h) = match state.mode {
Mode::Standard => (340.0, 560.0),
Mode::Standard => (360.0, 580.0),
Mode::Engineering => (420.0, 720.0),
Mode::Programmer => {
let bit_h = if state.show_bits {
match state.prog.word_size().bits() {
@@ -159,10 +238,22 @@ fn apply_crc_results(ui: &AppWindow, state: &State) {
fn refresh(ui: &AppWindow, state: &mut State) {
ui.set_mode(match state.mode {
Mode::Standard => 0,
Mode::Programmer => 1,
Mode::Convert => 2,
Mode::Crc => 3,
Mode::Engineering => 1,
Mode::Programmer => 2,
Mode::Convert => 3,
Mode::Crc => 4,
});
ui.set_mode_title(
match state.mode {
Mode::Standard => "Standard",
Mode::Engineering => "Engineering",
Mode::Programmer => "Programmer",
Mode::Convert => "Convert",
Mode::Crc => "CRC",
}
.into(),
);
ui.set_menu_open(false);
fit_window(ui, state);
@@ -176,6 +267,13 @@ fn refresh(ui: &AppWindow, state: &mut State) {
StdPanel::Formula => 1,
});
}
Mode::Engineering => {
ui.set_display_text(state.eng.display().into());
ui.set_expression_text(state.eng.expression().into());
ui.set_has_memory(state.eng.has_memory());
ui.set_eng_angle_label(state.eng.angle_mode().label().into());
ui.set_eng_second(state.eng.second());
}
Mode::Programmer => {
ui.set_display_text(state.prog.display().into());
ui.set_expression_text(state.prog.expression().into());
@@ -246,6 +344,10 @@ fn handle_key(state: &mut State, id: &str) {
state.mode = Mode::Standard;
return;
}
"mode:eng" => {
state.mode = Mode::Engineering;
return;
}
"mode:prog" => {
state.mode = Mode::Programmer;
return;
@@ -263,12 +365,61 @@ fn handle_key(state: &mut State, id: &str) {
match state.mode {
Mode::Standard => handle_standard(&mut state.std, id),
Mode::Engineering => handle_engineering(&mut state.eng, id),
Mode::Programmer => handle_programmer(state, id),
Mode::Convert => handle_convert(&mut state.conv, id),
Mode::Crc => {}
}
}
fn handle_engineering(calc: &mut Engineering, id: &str) {
match id {
"0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" => {
calc.input_digit(id.chars().next().unwrap());
}
"." | "," => calc.input_dot(),
"+" => calc.set_op("+"),
"-" | "" => calc.set_op(""),
"*" | "×" => calc.set_op("×"),
"/" | "÷" => calc.set_op("÷"),
"^" => calc.set_op("^"),
"=" => calc.equals(),
"(" => calc.paren_open(),
")" => calc.paren_close(),
"CE" => calc.clear_entry(),
"C" | "clear" => calc.clear_all(),
"BS" => calc.backspace(),
"neg" => calc.negate(),
"eng:angle" => calc.cycle_angle(),
"eng:2nd" => calc.toggle_second(),
"eng:pi" => calc.insert_const("π"),
"eng:e" => calc.insert_const("e"),
"eng:sin" => calc.insert_func("sin"),
"eng:cos" => calc.insert_func("cos"),
"eng:tan" => calc.insert_func("tan"),
"eng:asin" => calc.insert_func("asin"),
"eng:acos" => calc.insert_func("acos"),
"eng:atan" => calc.insert_func("atan"),
"eng:sqrt" => calc.insert_func("sqrt"),
"eng:cbrt" => calc.insert_func("cbrt"),
"eng:sq" => calc.insert_func("sq"),
"eng:cube" => calc.insert_func("cube"),
"eng:inv" => calc.insert_func("inv"),
"eng:abs" => calc.insert_func("abs"),
"eng:fact" => calc.insert_func("fact"),
"eng:exp" => calc.insert_func("exp"),
"eng:tenpow" => calc.insert_func("tenpow"),
"eng:log" => calc.insert_func("log"),
"eng:ln" => calc.insert_func("ln"),
"MC" => calc.memory_clear(),
"MR" => calc.memory_recall(),
"M+" => calc.memory_add(),
"M-" => calc.memory_sub(),
"MS" => calc.memory_store(),
_ => {}
}
}
fn handle_standard(calc: &mut Calculator, id: &str) {
match id {
"panel:std" => calc.set_panel(StdPanel::Standard),
+256 -38
View File
@@ -165,6 +165,30 @@ component EqBtn inherits Rectangle {
}
}
component MenuRow inherits Rectangle {
callback clicked();
in property <string> label;
in property <bool> active;
height: 36px;
background: root.active ? #2f3a42 : (touch.has-hover ? #323232 : transparent);
Text {
text: root.label;
color: root.active ? #60cdff : #e0e0e0;
font-size: 14px;
font-weight: root.active ? 700 : 500;
vertical-alignment: center;
horizontal-alignment: left;
x: 14px;
width: parent.width - 28px;
height: 100%;
}
touch := TouchArea {
clicked => { root.clicked(); }
}
}
component ModeTab inherits Rectangle {
callback clicked();
in property <string> label;
@@ -303,8 +327,10 @@ export component AppWindow inherits Window {
min-height: 520px;
background: #202020;
// 0 = standard, 1 = programmer, 2 = convert, 3 = crc
// 0=standard 1=engineering 2=programmer 3=convert 4=crc
in-out property <int> mode: 0;
in-out property <bool> menu-open: false;
in property <string> mode-title: "Standard";
in property <string> display-text: "0";
in property <string> expression-text: "";
@@ -312,6 +338,10 @@ export component AppWindow inherits Window {
// 0 = classic standard, 1 = formula
in property <int> std-panel: 0;
// Engineering
in property <string> eng-angle-label: "DEG";
in property <bool> eng-second: false;
in property <string> hex-text: "0";
in property <string> dec-text: "0";
in property <string> oct-text: "0";
@@ -367,44 +397,130 @@ export component AppWindow inherits Window {
root.key-pressed(id);
}
property <bool> hex-digits: root.mode == 1 && root.active-base == 0;
property <bool> allow-2: root.mode == 0 || root.active-base <= 2;
property <bool> allow-8: root.mode == 0 || root.active-base <= 1;
property <bool> allow-a: root.mode == 1 && root.active-base == 0;
property <bool> hex-digits: root.mode == 2 && root.active-base == 0;
property <bool> allow-2: root.mode == 0 || root.mode == 1 || root.active-base <= 2;
property <bool> allow-8: root.mode == 0 || root.mode == 1 || root.active-base <= 1;
property <bool> allow-a: root.mode == 2 && root.active-base == 0;
VerticalLayout {
padding: 12px;
spacing: 6px;
HorizontalLayout {
spacing: 6px;
height: 30px;
ModeTab {
label: "Standard";
active: root.mode == 0;
clicked => { root.press("mode:std"); }
spacing: 10px;
height: 34px;
Rectangle {
width: 34px;
height: 34px;
border-radius: 4px;
background: menu-touch.has-hover || root.menu-open ? #3b3b3b : transparent;
// Drawn icon — Unicode ☰ is missing in many Windows fonts.
VerticalLayout {
padding-top: 9px;
padding-bottom: 9px;
padding-left: 8px;
padding-right: 8px;
spacing: 4px;
Rectangle { height: 2px; background: #e8e8e8; border-radius: 1px; }
Rectangle { height: 2px; background: #e8e8e8; border-radius: 1px; }
Rectangle { height: 2px; background: #e8e8e8; border-radius: 1px; }
}
menu-touch := TouchArea {
clicked => {
root.menu-open = !root.menu-open;
if (root.menu-open) {
mode-menu.show();
} else {
mode-menu.close();
}
}
}
mode-menu := PopupWindow {
x: 0;
y: 38px;
width: 210px;
height: 220px;
close-on-click: true;
Rectangle {
background: #2a2a2a;
border-radius: 6px;
border-width: 1px;
border-color: #404040;
VerticalLayout {
padding: 6px;
spacing: 2px;
MenuRow {
label: "Standard";
active: root.mode == 0;
clicked => {
root.press("mode:std");
root.menu-open = false;
mode-menu.close();
}
}
MenuRow {
label: "Engineering";
active: root.mode == 1;
clicked => {
root.press("mode:eng");
root.menu-open = false;
mode-menu.close();
}
}
MenuRow {
label: "Programmer";
active: root.mode == 2;
clicked => {
root.press("mode:prog");
root.menu-open = false;
mode-menu.close();
}
}
MenuRow {
label: "Convert";
active: root.mode == 3;
clicked => {
root.press("mode:conv");
root.menu-open = false;
mode-menu.close();
}
}
MenuRow {
label: "CRC";
active: root.mode == 4;
clicked => {
root.press("mode:crc");
root.menu-open = false;
mode-menu.close();
}
}
}
}
}
}
ModeTab {
label: "Programmer";
active: root.mode == 1;
clicked => { root.press("mode:prog"); }
}
ModeTab {
label: "Convert";
active: root.mode == 2;
clicked => { root.press("mode:conv"); }
}
ModeTab {
label: "CRC";
active: root.mode == 3;
clicked => { root.press("mode:crc"); }
Text {
text: root.mode-title;
color: white;
font-size: 18px;
font-weight: 700;
vertical-alignment: center;
horizontal-stretch: 1;
}
}
if root.mode == 0 || root.mode == 1: HorizontalLayout {
if root.mode == 0 || root.mode == 1 || root.mode == 2: HorizontalLayout {
height: 18px;
Text {
text: root.has-memory && root.mode == 0 ? "M" : "";
text: root.has-memory && (root.mode == 0 || root.mode == 1) ? "M" : "";
color: #b4b4b4;
font-size: 12px;
horizontal-alignment: left;
@@ -412,7 +528,7 @@ export component AppWindow inherits Window {
width: 16px;
}
Text {
text: root.mode == 1 ? root.word-size-label : "";
text: root.mode == 2 ? root.word-size-label : "";
color: #60cdff;
font-size: 11px;
vertical-alignment: center;
@@ -426,18 +542,18 @@ export component AppWindow inherits Window {
}
}
if root.mode == 0 || root.mode == 1: Text {
if root.mode == 0 || root.mode == 1 || root.mode == 2: Text {
text: root.display-text;
color: white;
font-size: root.mode == 1 ? 28px : 34px;
font-size: root.mode == 2 ? 28px : 34px;
font-weight: 700;
horizontal-alignment: right;
vertical-alignment: center;
height: root.mode == 1 ? 40px : 48px;
height: root.mode == 2 ? 40px : 48px;
overflow: elide;
}
if root.mode == 3: VerticalLayout {
if root.mode == 4: VerticalLayout {
spacing: 10px;
Text {
@@ -594,7 +710,7 @@ export component AppWindow inherits Window {
}
}
if root.mode == 2: VerticalLayout {
if root.mode == 3: VerticalLayout {
spacing: 8px;
HorizontalLayout {
@@ -956,7 +1072,7 @@ export component AppWindow inherits Window {
}
}
if root.mode == 1: VerticalLayout {
if root.mode == 2: VerticalLayout {
spacing: 2px;
BaseRow {
label: "HEX";
@@ -984,7 +1100,7 @@ export component AppWindow inherits Window {
}
}
if root.mode == 1 && root.show-bits: VerticalLayout {
if root.mode == 2 && root.show-bits: VerticalLayout {
spacing: 2px;
padding-top: 4px;
padding-bottom: 4px;
@@ -1007,6 +1123,108 @@ export component AppWindow inherits Window {
}
}
if root.mode == 1: HorizontalLayout {
spacing: 6px;
height: 32px;
FuncBtn {
label: root.eng-angle-label;
clicked => { root.press("eng:angle"); }
}
MemBtn { label: "MC"; clicked => { root.press("MC"); } }
MemBtn { label: "MR"; clicked => { root.press("MR"); } }
MemBtn { label: "M+"; clicked => { root.press("M+"); } }
MemBtn { label: "M-"; clicked => { root.press("M-"); } }
MemBtn { label: "MS"; clicked => { root.press("MS"); } }
}
if root.mode == 1: GridLayout {
spacing: 4px;
Row {
FuncBtn {
min-h: 32px;
label: root.eng-second ? "2nd●" : "2nd";
clicked => { root.press("eng:2nd"); }
}
FuncBtn { min-h: 32px; label: "π"; clicked => { root.press("eng:pi"); } }
FuncBtn { min-h: 32px; label: "e"; clicked => { root.press("eng:e"); } }
FuncBtn { min-h: 32px; label: "C"; clicked => { root.press("C"); } }
FuncBtn { min-h: 32px; label: "⌫"; clicked => { root.press("BS"); } }
}
Row {
FuncBtn {
min-h: 32px;
label: root.eng-second ? "sin⁻¹" : "sin";
clicked => { root.press(root.eng-second ? "eng:asin" : "eng:sin"); }
}
FuncBtn {
min-h: 32px;
label: root.eng-second ? "cos⁻¹" : "cos";
clicked => { root.press(root.eng-second ? "eng:acos" : "eng:cos"); }
}
FuncBtn {
min-h: 32px;
label: root.eng-second ? "tan⁻¹" : "tan";
clicked => { root.press(root.eng-second ? "eng:atan" : "eng:tan"); }
}
FuncBtn {
min-h: 32px;
label: root.eng-second ? "³√x" : "√x";
clicked => { root.press(root.eng-second ? "eng:cbrt" : "eng:sqrt"); }
}
FuncBtn { min-h: 32px; label: "÷"; clicked => { root.press("/"); } }
}
Row {
FuncBtn {
min-h: 32px;
label: root.eng-second ? "x³" : "x²";
clicked => { root.press(root.eng-second ? "eng:cube" : "eng:sq"); }
}
FuncBtn { min-h: 32px; label: "1/x"; clicked => { root.press("eng:inv"); } }
FuncBtn { min-h: 32px; label: "|x|"; clicked => { root.press("eng:abs"); } }
FuncBtn { min-h: 32px; label: "n!"; clicked => { root.press("eng:fact"); } }
FuncBtn { min-h: 32px; label: "×"; clicked => { root.press("*"); } }
}
Row {
FuncBtn { min-h: 32px; label: "x^y"; clicked => { root.press("^"); } }
FuncBtn { min-h: 32px; label: "("; clicked => { root.press("("); } }
FuncBtn { min-h: 32px; label: ")"; clicked => { root.press(")"); } }
FuncBtn { min-h: 32px; label: "CE"; clicked => { root.press("CE"); } }
FuncBtn { min-h: 32px; label: ""; clicked => { root.press("-"); } }
}
Row {
FuncBtn {
min-h: 32px;
label: root.eng-second ? "eˣ" : "10ˣ";
clicked => { root.press(root.eng-second ? "eng:exp" : "eng:tenpow"); }
}
DigitBtn { min-h: 32px; label: "7"; clicked => { root.press("7"); } }
DigitBtn { min-h: 32px; label: "8"; clicked => { root.press("8"); } }
DigitBtn { min-h: 32px; label: "9"; clicked => { root.press("9"); } }
FuncBtn { min-h: 32px; label: "+"; clicked => { root.press("+"); } }
}
Row {
FuncBtn { min-h: 32px; label: "log"; clicked => { root.press("eng:log"); } }
DigitBtn { min-h: 32px; label: "4"; clicked => { root.press("4"); } }
DigitBtn { min-h: 32px; label: "5"; clicked => { root.press("5"); } }
DigitBtn { min-h: 32px; label: "6"; clicked => { root.press("6"); } }
DigitBtn { min-h: 32px; label: "±"; clicked => { root.press("neg"); } }
}
Row {
FuncBtn { min-h: 32px; label: "ln"; clicked => { root.press("eng:ln"); } }
DigitBtn { min-h: 32px; label: "1"; clicked => { root.press("1"); } }
DigitBtn { min-h: 32px; label: "2"; clicked => { root.press("2"); } }
DigitBtn { min-h: 32px; label: "3"; clicked => { root.press("3"); } }
EqBtn { min-h: 32px; clicked => { root.press("="); } }
}
Row {
DigitBtn { min-h: 32px; label: "0"; clicked => { root.press("0"); } }
DigitBtn { min-h: 32px; label: "."; clicked => { root.press("."); } }
FuncBtn { min-h: 32px; label: "exp"; clicked => { root.press("eng:exp"); } }
Rectangle { min-height: 32px; background: transparent; }
Rectangle { min-height: 32px; background: transparent; }
}
}
if root.mode == 0: HorizontalLayout {
spacing: 6px;
height: 30px;
@@ -1074,7 +1292,7 @@ export component AppWindow inherits Window {
}
}
if root.mode == 1: GridLayout {
if root.mode == 2: GridLayout {
spacing: 4px;
Row {
FuncBtn { min-h: 34px; label: root.word-size-label; clicked => { root.press("word"); } }
@@ -1140,7 +1358,7 @@ export component AppWindow inherits Window {
forward-focus: key-handler;
key-handler := FocusScope {
key-pressed(event) => {
if (root.mode == 3) {
if (root.mode == 4) {
return reject;
}
if (event.text == Key.Return) {
@@ -1152,7 +1370,7 @@ export component AppWindow inherits Window {
return accept;
}
if (event.text == Key.Escape) {
root.press(root.mode == 0 ? "C" : "clear");
root.press(root.mode == 0 || root.mode == 1 ? "C" : "clear");
return accept;
}
if (event.text == Key.Delete) {