diff --git a/.cargo/config.toml b/.cargo/config.toml index 5ed6338..bc87673 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -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++", +] diff --git a/.gitignore b/.gitignore index f243e4c..5e55dbd 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /dist/rcalc.AppDir/ /dist/.glibc-build.txt .rcalc_last_bump +/target-windows diff --git a/Cargo.lock b/Cargo.lock index 14ba08c..768c2b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3904,7 +3904,7 @@ dependencies = [ [[package]] name = "rcalc" -version = "0.1.4" +version = "0.1.11" dependencies = [ "slint", "slint-build", diff --git a/Cargo.toml b/Cargo.toml index e4dc672..36addfb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rcalc" -version = "0.1.5" +version = "0.1.12" edition = "2021" build = "build.rs" diff --git a/dist/libwinpthread-1.dll b/dist/libwinpthread-1.dll new file mode 100755 index 0000000..d87e531 Binary files /dev/null and b/dist/libwinpthread-1.dll differ diff --git a/dist/rcalc b/dist/rcalc index d3fb50e..c28d62c 100755 Binary files a/dist/rcalc and b/dist/rcalc differ diff --git a/dist/rcalc.exe b/dist/rcalc.exe index 7669943..6628869 100755 Binary files a/dist/rcalc.exe and b/dist/rcalc.exe differ diff --git a/dist/win_rcalc.zip b/dist/win_rcalc.zip index 0f38a3e..07759c9 100644 Binary files a/dist/win_rcalc.zip and b/dist/win_rcalc.zip differ diff --git a/dist/win_rcalc_019.zip b/dist/win_rcalc_019.zip new file mode 100644 index 0000000..06bd2fa Binary files /dev/null and b/dist/win_rcalc_019.zip differ diff --git a/src/engineering.rs b/src/engineering.rs new file mode 100644 index 0000000..2925e11 --- /dev/null +++ b/src/engineering.rs @@ -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, + 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::() { + 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 { + 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 { + 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, SciError> { + let chars: Vec = 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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"); + } +} diff --git a/src/main.rs b/src/main.rs index 865916f..28bb7eb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 { + 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 { let items: Vec = row .iter() @@ -98,9 +175,10 @@ fn bit_row_model(row: &[(bool, u32)]) -> ModelRc { 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), diff --git a/ui/app.slint b/ui/app.slint index dd5dc80..f95ab8a 100644 --- a/ui/app.slint +++ b/ui/app.slint @@ -165,6 +165,30 @@ component EqBtn inherits Rectangle { } } +component MenuRow inherits Rectangle { + callback clicked(); + in property label; + in property 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 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 mode: 0; + in-out property menu-open: false; + in property mode-title: "Standard"; in property display-text: "0"; in property expression-text: ""; @@ -312,6 +338,10 @@ export component AppWindow inherits Window { // 0 = classic standard, 1 = formula in property std-panel: 0; + // Engineering + in property eng-angle-label: "DEG"; + in property eng-second: false; + in property hex-text: "0"; in property dec-text: "0"; in property oct-text: "0"; @@ -367,44 +397,130 @@ export component AppWindow inherits Window { root.key-pressed(id); } - property hex-digits: root.mode == 1 && root.active-base == 0; - property allow-2: root.mode == 0 || root.active-base <= 2; - property allow-8: root.mode == 0 || root.active-base <= 1; - property allow-a: root.mode == 1 && root.active-base == 0; + property hex-digits: root.mode == 2 && root.active-base == 0; + property allow-2: root.mode == 0 || root.mode == 1 || root.active-base <= 2; + property allow-8: root.mode == 0 || root.mode == 1 || root.active-base <= 1; + property 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) {