diff --git a/calculator.go b/calculator.go index 92f11f4..4810ad3 100644 --- a/calculator.go +++ b/calculator.go @@ -21,14 +21,15 @@ type Display struct { // ---- service --------------------------------------------------------------- type CalcService struct { - e *engine.Engine - hasEntry bool - buf string - expBuf string - hasDot bool - hasSign bool - inExp bool - armed string // "", "f", "g", "STO", "RCL" + e *engine.Engine + hasEntry bool + buf string + expBuf string + hasDot bool + hasSign bool + inExp bool + armed string // "", "f", "g", "STO", "RCL" + undoState *engine.EngineState } type KeyResult struct { @@ -65,6 +66,12 @@ func (c *CalcService) PressKey(input KeyInput) KeyResult { isDigit = true } + // snapshot before every state-changing operation for single-level undo + if op != "f" && op != "g" && op != "STO" && op != "RCL" && !isDigit && op != "." && op != "CHS" && op != "EEX" && op != "ENTER" && op != "CLx" { + s := c.e.Snapshot() + c.undoState = &s + } + switch { case c.armed == "f": c.fPrefixed(op, arg, input.ArgS) @@ -80,7 +87,6 @@ func (c *CalcService) PressKey(input KeyInput) KeyResult { c.armed = "" case op == "f" || op == "g": c.armed = op - c.clearEntry() return c.state() case op == "STO" || op == "RCL": c.armed = op @@ -125,7 +131,6 @@ func (c *CalcService) PressKey(input KeyInput) KeyResult { c.e.LastXRecall() case op == "f" || op == "g": c.armed = op - c.clearEntry() return c.state() case op == "STO" || op == "RCL": c.armed = op @@ -480,6 +485,31 @@ func (c *CalcService) gPrefixed(op string, arg float64, argS string) { case "BST": case "GTO": case "R/S": + case "+": + c.finishEntry() + c.e.LastXRecall() + case "×": + c.finishEntry() + c.e.Sqr() + case "÷": + if c.undoState != nil { + c.e.Restore(*c.undoState) + c.undoState = nil + } + case "−": + c.backspace() + case "PMT": + c.finishEntry() + if c.e.FinCfCnt < 10 { + c.e.FinCFj[c.e.FinCfCnt] = c.e.X + c.e.FinCfCnt++ + } + case "FV": + c.finishEntry() + n := int(c.e.X) + if n > 0 && c.e.FinCfCnt > 0 { + c.e.FinNj[c.e.FinCfCnt-1] = n + } default: _ = argS } @@ -565,6 +595,29 @@ func (c *CalcService) clearEntry() { c.inExp = false } +func (c *CalcService) backspace() { + if !c.hasEntry { + // not in an active entry; start editing the current X value + c.buf = strconv.FormatFloat(c.e.X, 'f', -1, 64) + if len(c.buf) > 0 { + c.buf = c.buf[:len(c.buf)-1] + } + c.hasEntry = true + } else { + if c.inExp && len(c.expBuf) > 0 { + c.expBuf = c.expBuf[:len(c.expBuf)-1] + } else if !c.inExp && len(c.buf) > 0 { + c.buf = c.buf[:len(c.buf)-1] + } + } + if c.buf == "" || c.buf == "-" || c.buf == "." || c.buf == "-." { + c.clearEntry() + c.e.X = 0 + } else { + c.e.X = c.parseBuf() + } +} + func (c *CalcService) parseBuf() float64 { s := c.buf if c.inExp && c.expBuf != "" { @@ -669,15 +722,13 @@ func trimZero(s string) string { return s } i := 0 - if s[0] == '-' { + neg := s[0] == '-' + if neg { i = 1 } for i < len(s)-1 && s[i] == '0' && s[i+1] != '.' { i++ } - if i > 0 && s[0] == '-' { - return "-" + s[i:] - } return s[i:] } diff --git a/docs/screenshot.png b/docs/screenshot.png index 97d9c3f..6a95f7d 100644 Binary files a/docs/screenshot.png and b/docs/screenshot.png differ diff --git a/frontend/public/style.css b/frontend/public/style.css index dbec01f..4f35354 100644 --- a/frontend/public/style.css +++ b/frontend/public/style.css @@ -3,10 +3,10 @@ :root { --calc-w: 856px; --calc-h: 486px; - --key-w: 62px; - --key-h: 46px; + --key-w: 66px; + --key-h: 50px; --col-gap: 9px; - --row-gap: 48px; + --row-gap: 52px; --gold: #e8892b; --blue: #4fb6da; --key-text: #eceae4; @@ -204,17 +204,17 @@ body { /* primary label on key face */ .key .prim { - font-size: 15px; + font-size: 17px; font-weight: 600; line-height: 1; color: var(--key-text); } -.key .prim.sm { font-size: 12px; } -.key .prim.xs { font-size: 10.5px; } +.key .prim.sm { font-size: 14px; } +.key .prim.xs { font-size: 12px; } /* blue (g) label under the primary, on the key face */ .key .glabel { - font-size: 9px; + font-size: 13px; font-weight: 700; line-height: 1; color: var(--blue); @@ -242,7 +242,7 @@ body { 0 -3px 4px rgba(0,0,0,.28) inset, 0 2px 3px rgba(0,0,0,.55); } -.key.fkey .prim { color: #3a2408; font-weight: 800; font-size: 17px; } +.key.fkey .prim { color: #3a2408; font-weight: 800; font-size: 19px; } .key.gkey { background: linear-gradient(180deg, #34bce6 0%, #159fce 100%); box-shadow: @@ -250,7 +250,7 @@ body { 0 -3px 4px rgba(0,0,0,.28) inset, 0 2px 3px rgba(0,0,0,.55); } -.key.gkey .prim { color: #0a2f3d; font-weight: 800; font-size: 17px; } +.key.gkey .prim { color: #0a2f3d; font-weight: 800; font-size: 19px; } .key.armed { outline: 2px solid #f0b45a; outline-offset: 1px; } diff --git a/frontend/src/main.js b/frontend/src/main.js index 9f4a5c2..8d10eab 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -20,7 +20,7 @@ const KEYS = [ { r:1,c:7, prim:"7", f:"", g:"BEG", op:"7", fop:"FIX", gop:"BEG", fixArg:7 }, { r:1,c:8, prim:"8", f:"", g:"END", op:"8", fop:"FIX", gop:"END", fixArg:8 }, { r:1,c:9, prim:"9", f:"", g:"MEM", op:"9", fop:"FIX", gop:"MEM", fixArg:9 }, - { r:1,c:10, prim:"÷", f:"", g:"", op:"÷", fop:NOP, gop:NOP }, + { r:1,c:10, prim:"÷", f:"", g:"↩", op:"÷", fop:NOP, gop:"÷" }, // row 2 @@ -33,7 +33,7 @@ const KEYS = [ { r:2,c:7, prim:"4", f:"", g:"D.MY",op:"4", fop:"FIX", gop:"D.MY", fixArg:4 }, { r:2,c:8, prim:"5", f:"", g:"M.DY",op:"5", fop:"FIX", gop:"M.DY", fixArg:5 }, { r:2,c:9, prim:"6", f:"", g:"x̄w", op:"6", fop:"FIX", gop:"x̄w", fixArg:6 }, - { r:2,c:10, prim:"×", f:"", g:"", op:"×", fop:NOP, gop:NOP }, + { r:2,c:10, prim:"×", f:"", g:"x²", op:"×", fop:NOP, gop:"×" }, // row 3 { r:3,c:1, prim:"R/S", f:"P/R", g:"PSE", op:"R/S", fop:"P/R", gop:"PSE", primClass:"sm" }, @@ -41,11 +41,11 @@ const KEYS = [ { r:3,c:3, prim:"R↓", f:"PRGM", g:"GTO", op:"R↓", fop:"CLEAR PRGM", gop:"GTO" }, { r:3,c:4, prim:"x↔y", f:"FIN", g:"x≤y", op:"x↔y", fop:"CLEAR FIN", gop:"x≤y", primClass:"xs" }, { r:3,c:5, prim:"CLx", f:"REG", g:"x=0", op:"CLx", fop:"CLEAR REG", gop:"x=0", primClass:"sm" }, - { enter:true, r:3, c:6, prim:"ENTER", f:"PREFIX", g:"LSTx", op:"ENTER", fop:"CLEAR PREFIX", gop:"LSTx" }, + { enter:true, r:3, c:6, prim:"ENTER", f:"PREFIX", g:"", op:"ENTER", fop:"CLEAR PREFIX", gop:NOP }, { r:3,c:7, prim:"1", f:"", g:"x̂,r", op:"1", fop:NOP, gop:NOP }, { r:3,c:8, prim:"2", f:"", g:"ŷ,r", op:"2", fop:NOP, gop:"ŷ,r" }, { r:3,c:9, prim:"3", f:"", g:"n!", op:"3", fop:NOP, gop:"n!" }, - { r:3,c:10, prim:"−", f:"", g:"", op:"−", fop:NOP, gop:NOP }, + { r:3,c:10, prim:"−", f:"", g:"←", op:"−", fop:NOP, gop:"−" }, // row 4 { r:4,c:1, prim:"ON", f:"", g:"", op:"ON", fop:NOP, gop:NOP, primClass:"sm" }, { r:4,c:2, prim:"f", f:"", g:"", op:"f", special:"f" }, @@ -55,7 +55,7 @@ const KEYS = [ { r:4,c:7, prim:"0", f:"", g:"x̄", op:"0", fop:"FIX", gop:"x̄", fixArg:0 }, { r:4,c:8, prim:".", f:"", g:"s", op:".", fop:NOP, gop:"s" }, { r:4,c:9, prim:"Σ+", f:"", g:"Σ−", op:"Σ+", fop:NOP, gop:NOP, primClass:"sm" }, - { r:4,c:10, prim:"+", f:"", g:"", op:"+", fop:NOP, gop:NOP }, + { r:4,c:10, prim:"+", f:"", g:"LSTx", op:"+", fop:NOP, gop:"+" }, ]; // Groups drawn as gold brackets above the keys. @@ -95,6 +95,7 @@ const ANNUN = ["f", "g", "BEGIN", "RAD", "D.MY", "PRGM"]; function applyState(s) { const d = (s && s.display) || {}; const sign = d.sign && d.sign.trim() ? d.sign : ""; + signEl.textContent = sign; digitsEl.textContent = d.mantissa && d.mantissa.trim() ? d.mantissa.trim() : "0.00"; const flags = (d.flags || []).map(f => String(f).toUpperCase()); annunEl.innerHTML = ANNUN.map(a => {