diff --git a/Cargo.lock b/Cargo.lock index 768d827..268a3f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3904,7 +3904,7 @@ dependencies = [ [[package]] name = "rcalc" -version = "0.1.25" +version = "0.1.28" dependencies = [ "slint", "slint-build", diff --git a/Cargo.toml b/Cargo.toml index 7c9e55d..78b3d3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rcalc" -version = "0.1.25" +version = "0.1.28" edition = "2021" build = "build.rs" diff --git a/build.rs b/build.rs index d7c37fe..d1f9e75 100644 --- a/build.rs +++ b/build.rs @@ -1,7 +1,4 @@ use std::env; -use std::fs; -use std::path::PathBuf; -use std::time::Duration; fn main() { let version = resolve_app_version(); @@ -38,82 +35,10 @@ fn embed_windows_icon() { /// Window / package version shown as `Rcalc-{version}`. /// -/// On each `release` build the patch in `Cargo.toml` is incremented once -/// (`0.1.0` → `0.1.1` → …). If Cargo restarts the build script right after we -/// rewrite `Cargo.toml`, a short mtime guard prevents a double bump. +/// Always the version from `Cargo.toml`. Раньше каждая release-сборка +/// инкрементировала patch в Cargo.toml посреди сборки — из-за этого +/// имя AppImage (читается скриптом до сборки) отставало от версии, +/// зашитой в бинарник. Теперь версия меняется только вручную. fn resolve_app_version() -> String { - let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); - let pkg = env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.0.0".into()); - let profile = env::var("PROFILE").unwrap_or_default(); - - if profile != "release" { - return pkg; - } - - let cargo_toml = manifest_dir.join("Cargo.toml"); - let guard_path = manifest_dir.join(".rcalc_last_bump"); - let already = fs::read_to_string(&guard_path) - .map(|s| s.trim().to_string()) - .unwrap_or_default(); - - // Cargo re-invoked us because we just rewrote Cargo.toml — keep version. - if already == pkg { - if let Ok(meta) = fs::metadata(&cargo_toml) { - if let Ok(modified) = meta.modified() { - if modified.elapsed().unwrap_or(Duration::MAX) < Duration::from_secs(15) { - return pkg; - } - } - } - } - - let Some(next) = bump_patch(&pkg) else { - return pkg; - }; - - if let Err(e) = set_cargo_toml_version(&cargo_toml, &next) { - println!("cargo:warning=could not bump Cargo.toml version: {e}"); - return pkg; - } - let _ = fs::write(&guard_path, &next); - next -} - -fn bump_patch(version: &str) -> Option { - let mut parts: Vec<&str> = version.split('.').collect(); - if parts.is_empty() { - return None; - } - while parts.len() < 3 { - parts.push("0"); - } - let patch: u64 = parts[2] - .split(['-', '+']) - .next()? - .parse() - .ok()?; - Some(format!("{}.{}.{}", parts[0], parts[1], patch + 1)) -} - -fn set_cargo_toml_version(path: &PathBuf, version: &str) -> std::io::Result<()> { - let raw = fs::read_to_string(path)?; - let mut out = String::with_capacity(raw.len() + 8); - let mut replaced = false; - for line in raw.lines() { - if !replaced && line.starts_with("version") && line.contains('=') { - out.push_str(&format!("version = \"{version}\"")); - out.push('\n'); - replaced = true; - } else { - out.push_str(line); - out.push('\n'); - } - } - if !replaced { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "version field not found in Cargo.toml", - )); - } - fs::write(path, out) + env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.0.0".into()) } diff --git a/dist/rcalc b/dist/rcalc index 71faa5c..93b60c4 100755 Binary files a/dist/rcalc and b/dist/rcalc differ diff --git a/scripts/build-appimage.sh b/scripts/build-appimage.sh index 84b1c3a..ca195ee 100755 --- a/scripts/build-appimage.sh +++ b/scripts/build-appimage.sh @@ -101,6 +101,9 @@ package_appimage() { echo "Bundling libraries with linuxdeploy…" export APPIMAGE_EXTRACT_AND_RUN=1 export OUTPUT="${IMAGE_NAME}" + # Marker: mtime boundary to find the FRESH linuxdeploy output. + local marker="${OUT_DIR}/.pack-start" + touch "${marker}" ( cd "${OUT_DIR}" "${linuxdeploy}" \ @@ -111,11 +114,15 @@ package_appimage() { --output appimage ) - # linuxdeploy drops the AppImage next to AppDir (dist/) + # linuxdeploy drops the AppImage next to AppDir (dist/). Only consider files + # NEWER than the marker: a blind `find | head -1` with several older + # *.AppImage in dist/ renamed an OLD image OVER the freshly built one + # (rcalc-0.1.28 got shipped with a 0.1.25 binary from the first build). local built - built="$(find "${OUT_DIR}" -maxdepth 1 -name '*.AppImage' ! -name 'linuxdeploy*' | head -1)" + built="$(find "${OUT_DIR}" -maxdepth 1 -name '*.AppImage' ! -name 'linuxdeploy*' -newer "${marker}" | head -1)" + rm -f "${marker}" if [[ -z "$built" ]]; then - echo "error: AppImage was not produced" >&2 + echo "error: AppImage was not produced (linuxdeploy failed?)" >&2 exit 1 fi if [[ "$(basename "$built")" != "${IMAGE_NAME}" ]]; then diff --git a/src/engineering.rs b/src/engineering.rs index 680e5ce..7100516 100644 --- a/src/engineering.rs +++ b/src/engineering.rs @@ -389,6 +389,26 @@ impl Engineering { self.sync_expr(); } + /// Insert `× 10 ^` for scientific-notation entry: `1.5 [×10ˣ] 3 = 1500`. + /// Exponent may be negated with ± before `=` (`10 ^ −3` parses correctly). + pub fn mul_pow10(&mut self) { + if self.error.is_some() { + return; + } + if self.done { + // Continue from the result: 1500 [×10ˣ] 2 = 150000. + self.formula.clear(); + self.done = false; + self.typing = true; + } + self.flush_entry(); + self.formula.push_str(" × 10 ^ "); + self.entry = "0".into(); + self.typing = false; + self.second = false; + self.sync_expr(); + } + pub fn equals(&mut self) { if self.error.is_some() { return; @@ -884,6 +904,47 @@ mod tests { assert_eq!(e.display(), "1024"); } + #[test] + fn mul_pow10_basic() { + // 1.5 ×10ˣ 3 = 1500 (× 10 ^ с приоритетом ^ над ×). + let mut e = Engineering::new(); + e.input_digit('1'); + e.input_dot(); + e.input_digit('5'); + e.mul_pow10(); + e.input_digit('3'); + e.equals(); + assert_eq!(e.display(), "1500"); + } + + #[test] + fn mul_pow10_negative_exponent() { + // 2.5 ×10ˣ ±2 = 0.025. + let mut e = Engineering::new(); + e.input_digit('2'); + e.input_dot(); + e.input_digit('5'); + e.mul_pow10(); + e.input_digit('2'); + e.negate(); + e.equals(); + assert_eq!(e.display(), "0.025"); + } + + #[test] + fn mul_pow10_continues_result() { + // 2 × 3 = 6, затем 6 ×10ˣ 2 = 600. + let mut e = Engineering::new(); + e.input_digit('2'); + e.set_op("×"); + e.input_digit('3'); + e.equals(); + e.mul_pow10(); + e.input_digit('2'); + e.equals(); + assert_eq!(e.display(), "600"); + } + #[test] fn ten_pow_negative_not_zero() { // 10 ^ −50 ≈ 1e-50 was snapped to "0" by format_num. diff --git a/src/main.rs b/src/main.rs index 82e7c5e..55a651e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -532,6 +532,7 @@ fn handle_engineering(calc: &mut Engineering, id: &str) { "eng:fact" => calc.insert_func("fact"), "eng:exp" => calc.insert_func("exp"), "eng:tenpow" => calc.insert_func("tenpow"), + "eng:mul10pow" => calc.mul_pow10(), "eng:log" => calc.insert_func("log"), "eng:ln" => calc.insert_func("ln"), "MC" => calc.memory_clear(), diff --git a/ui/app.slint b/ui/app.slint index 0f8865c..f1d0b7f 100644 --- a/ui/app.slint +++ b/ui/app.slint @@ -1211,7 +1211,7 @@ export component AppWindow inherits Window { label: root.eng-second ? "³√x" : "√x"; clicked => { root.press(root.eng-second ? "eng:cbrt" : "eng:sqrt"); } } - FuncBtn { min-h: UiStyle.btn-h-sm; label: "÷"; clicked => { root.press("/"); } } + FuncBtn { min-h: UiStyle.btn-h-sm; label: "x*10^y"; clicked => { root.press("eng:mul10pow"); } } } Row { FuncBtn { @@ -1222,14 +1222,14 @@ export component AppWindow inherits Window { FuncBtn { min-h: UiStyle.btn-h-sm; label: "1/x"; clicked => { root.press("eng:inv"); } } FuncBtn { min-h: UiStyle.btn-h-sm; label: "|x|"; clicked => { root.press("eng:abs"); } } FuncBtn { min-h: UiStyle.btn-h-sm; label: "n!"; clicked => { root.press("eng:fact"); } } - FuncBtn { min-h: UiStyle.btn-h-sm; label: "×"; clicked => { root.press("*"); } } + FuncBtn { min-h: UiStyle.btn-h-sm; label: "÷"; clicked => { root.press("/"); } } } Row { FuncBtn { min-h: UiStyle.btn-h-sm; label: "x^y"; clicked => { root.press("^"); } } FuncBtn { min-h: UiStyle.btn-h-sm; label: "("; clicked => { root.press("("); } } FuncBtn { min-h: UiStyle.btn-h-sm; label: ")"; clicked => { root.press(")"); } } FuncBtn { min-h: UiStyle.btn-h-sm; label: "CE"; clicked => { root.press("CE"); } } - FuncBtn { min-h: UiStyle.btn-h-sm; label: "−"; clicked => { root.press("-"); } } + FuncBtn { min-h: UiStyle.btn-h-sm; label: "×"; clicked => { root.press("*"); } } } Row { FuncBtn { @@ -1240,28 +1240,28 @@ export component AppWindow inherits Window { DigitBtn { min-h: UiStyle.btn-h-sm; label: "7"; clicked => { root.press("7"); } } DigitBtn { min-h: UiStyle.btn-h-sm; label: "8"; clicked => { root.press("8"); } } DigitBtn { min-h: UiStyle.btn-h-sm; label: "9"; clicked => { root.press("9"); } } - FuncBtn { min-h: UiStyle.btn-h-sm; label: "+"; clicked => { root.press("+"); } } + FuncBtn { min-h: UiStyle.btn-h-sm; label: "−"; clicked => { root.press("-"); } } } Row { FuncBtn { min-h: UiStyle.btn-h-sm; label: "log"; clicked => { root.press("eng:log"); } } DigitBtn { min-h: UiStyle.btn-h-sm; label: "4"; clicked => { root.press("4"); } } DigitBtn { min-h: UiStyle.btn-h-sm; label: "5"; clicked => { root.press("5"); } } DigitBtn { min-h: UiStyle.btn-h-sm; label: "6"; clicked => { root.press("6"); } } - DigitBtn { min-h: UiStyle.btn-h-sm; label: "±"; clicked => { root.press("neg"); } } + FuncBtn { min-h: UiStyle.btn-h-sm; label: "+"; clicked => { root.press("+"); } } } Row { FuncBtn { min-h: UiStyle.btn-h-sm; label: "ln"; clicked => { root.press("eng:ln"); } } DigitBtn { min-h: UiStyle.btn-h-sm; label: "1"; clicked => { root.press("1"); } } DigitBtn { min-h: UiStyle.btn-h-sm; label: "2"; clicked => { root.press("2"); } } DigitBtn { min-h: UiStyle.btn-h-sm; label: "3"; clicked => { root.press("3"); } } - EqBtn { min-h: UiStyle.btn-h-sm; clicked => { root.press("="); } } + DigitBtn { min-h: UiStyle.btn-h-sm; label: "±"; clicked => { root.press("neg"); } } } Row { + FuncBtn { min-h: UiStyle.btn-h-sm; label: "exp"; clicked => { root.press("eng:exp"); } } DigitBtn { min-h: UiStyle.btn-h-sm; label: "0"; clicked => { root.press("0"); } } DigitBtn { min-h: UiStyle.btn-h-sm; label: "."; clicked => { root.press("."); } } - FuncBtn { min-h: UiStyle.btn-h-sm; label: "exp"; clicked => { root.press("eng:exp"); } } - Rectangle { min-height: 32px; background: transparent; } Rectangle { min-height: 32px; background: transparent; } + EqBtn { min-h: UiStyle.btn-h-sm; clicked => { root.press("="); } } } } @@ -1293,10 +1293,17 @@ export component AppWindow inherits Window { if root.mode == 0: GridLayout { spacing: 6px; Row { - if root.std-panel == 0: FuncBtn { label: "%"; clicked => { root.press("%"); } } - if root.std-panel == 1: FuncBtn { label: "("; clicked => { root.press("("); } } - if root.std-panel == 0: FuncBtn { label: "CE"; clicked => { root.press("CE"); } } - if root.std-panel == 1: FuncBtn { label: ")"; clicked => { root.press(")"); } } + // `if` внутри Row у GridLayout ломает раскладку Slint + // (RepeatedItemTree::grid_layout_input_data) — кнопки теряются. + // Поэтому одна кнопка с тернарником вместо пары условных. + FuncBtn { + label: root.std-panel == 0 ? "%" : "("; + clicked => { root.press(root.std-panel == 0 ? "%" : "("); } + } + FuncBtn { + label: root.std-panel == 0 ? "CE" : ")"; + clicked => { root.press(root.std-panel == 0 ? "CE" : ")"); } + } FuncBtn { label: "C"; clicked => { root.press("C"); } } FuncBtn { label: "⌫"; clicked => { root.press("BS"); } } }