1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
|
## A calculator whose arithmetic and layout both live in Nim.
##
## The keypad is described as data and walked to build the tree, so the rows
## in the source are the rows on screen. State that has no button — a pending
## operator, an error — simply changes what gets described next frame.
import std/[math, strformat, strutils]
import cosmicnim
type
Op = enum
opNone = "", opAdd = "+", opSub = "−", opMul = "×", opDiv = "÷"
CalcError* = enum
## The display can only ever say "Error", so the reason rides alongside
## it. Each value is its own message.
errNone = ""
errDivZero = "Cannot divide by zero"
errRange = "Result is out of range"
Key = object
label: string
id: int32
style: ButtonStyle
Calc* = object
entry: string ## digits being typed; empty means "showing the accumulator"
acc: float
pending: Op
startNew: bool ## next digit starts a fresh entry
error: CalcError
const
Cols = 4
Rows = 5
# The display block is a fixed height whatever it contains, so the keypad
# does not walk up and down as the pending-operation line comes and goes.
CaptionH = 26.0
ValueH = 64.0
MinKeyW = 56.0
MinKeyH = 44.0
# 0..9 are the digit keys, so their id is their value.
IdDot* = 10'i32
IdAdd* = 11'i32
IdSub* = 12'i32
IdMul* = 13'i32
IdDiv* = 14'i32
IdEquals* = 15'i32
IdClear* = 16'i32
IdSign* = 17'i32
IdPercent* = 18'i32
IdBackspace* = 19'i32
Keypad: array[5, array[4, Key]] = [
[Key(label: "C", id: IdClear, style: ButtonDestructive),
Key(label: "±", id: IdSign, style: ButtonText),
Key(label: "%", id: IdPercent, style: ButtonText),
Key(label: "÷", id: IdDiv, style: ButtonStandard)],
[Key(label: "7", id: 7, style: ButtonStandard),
Key(label: "8", id: 8, style: ButtonStandard),
Key(label: "9", id: 9, style: ButtonStandard),
Key(label: "×", id: IdMul, style: ButtonStandard)],
[Key(label: "4", id: 4, style: ButtonStandard),
Key(label: "5", id: 5, style: ButtonStandard),
Key(label: "6", id: 6, style: ButtonStandard),
Key(label: "−", id: IdSub, style: ButtonStandard)],
[Key(label: "1", id: 1, style: ButtonStandard),
Key(label: "2", id: 2, style: ButtonStandard),
Key(label: "3", id: 3, style: ButtonStandard),
Key(label: "+", id: IdAdd, style: ButtonStandard)],
[Key(label: "0", id: 0, style: ButtonStandard),
Key(label: ".", id: IdDot, style: ButtonStandard),
Key(label: "=", id: IdEquals, style: ButtonSuggested),
Key(label: "", id: -1, style: ButtonText)], # id -1: an inert spacer
]
proc newCalc*(): Calc =
## A calculator showing zero, waiting for its first digit.
Calc(startNew: true)
proc format(v: float): string =
## Integers read as integers; everything else loses its trailing zeros.
if v != v:
result = "NaN"
elif v in [Inf, NegInf]:
result = "∞"
elif v == trunc(v) and abs(v) < 1e15:
result = $int64(v)
else:
result = formatFloat(v, ffDefault, 12)
result.trimZeros()
proc current(c: Calc): float =
## What the display is showing, as a number.
if c.entry.len > 0: parseFloat(c.entry) else: c.acc
proc failed*(c: Calc): bool = c.error != errNone
proc reason*(c: Calc): string =
## Why the display says Error, or "" when it does not.
$c.error
proc display*(c: Calc): string =
if c.failed: "Error"
elif c.entry.len > 0: c.entry
else: format(c.acc)
proc resolve(c: var Calc) =
## Fold the entry into the accumulator using the pending operator.
let rhs = c.current
case c.pending
of opNone: c.acc = rhs
of opAdd: c.acc = c.acc + rhs
of opSub: c.acc = c.acc - rhs
of opMul: c.acc = c.acc * rhs
of opDiv:
if rhs == 0.0: c.error = errDivZero
else: c.acc = c.acc / rhs
# Catch a result that has run off the end of what a float can say, so it
# shows a reason rather than silently becoming inf or NaN.
if c.error == errNone and (c.acc != c.acc or abs(c.acc) == Inf):
c.error = errRange
c.entry = ""
proc digit(c: var Calc; d: int32) =
if c.startNew:
c.entry = ""
c.startNew = false
if c.entry == "0": c.entry = "" # no leading zeros
c.entry.add $d
proc onPress*(ctx: pointer; id: int32) {.cdecl.} =
let c = cast[ptr Calc](ctx)
# Clear is the only key that works once the display says Error.
if id == IdClear:
c[] = newCalc()
return
if c[].failed:
return
case id
of 0'i32 .. 9'i32:
c[].digit(id)
of IdDot:
if c.startNew:
c.entry = "0"
c.startNew = false
if c.entry.len == 0: c.entry = "0"
if '.' notin c.entry: c.entry.add '.'
of IdAdd, IdSub, IdMul, IdDiv:
c[].resolve()
if not c[].failed:
c.pending = case id
of IdAdd: opAdd
of IdSub: opSub
of IdMul: opMul
else: opDiv
c.startNew = true
of IdEquals:
c[].resolve()
c.pending = opNone
c.startNew = true
of IdSign:
if c.entry.len > 0:
if c.entry.startsWith('-'): c.entry = c.entry[1 .. ^1]
else: c.entry = "-" & c.entry
else:
c.acc = -c.acc
of IdBackspace:
# Only the typed entry can be rubbed out; a computed result cannot.
if c.entry.len > 0:
c.entry.setLen(c.entry.len - 1)
if c.entry == "-": c.entry = ""
of IdPercent:
if c.entry.len > 0: c.entry = format(parseFloat(c.entry) / 100.0)
else: c.acc = c.acc / 100.0
else:
discard
proc keyToId*(key: string): int32 =
## The id a key press stands for, or -1 for a key the calculator ignores.
## Both spellings of each operator are accepted: the ASCII one a keyboard
## actually produces, and the typographic one printed on the key.
if key.len == 1 and key[0] in '0'..'9':
return int32(ord(key[0]) - ord('0'))
case key
of ".", ",": IdDot
of "+": IdAdd
of "-", "−": IdSub
of "*", "x", "×": IdMul
of "/", "÷": IdDiv
of "=", "Enter", "NumpadEnter": IdEquals
of "c", "C", "Escape", "Delete": IdClear
of "Backspace": IdBackspace
of "%": IdPercent
of "n", "N": IdSign
else: -1
proc onKey(ctx: pointer; key: cstring) {.cdecl.} =
let id = keyToId($key)
if id >= 0:
onPress(ctx, id)
proc onView(ctx: pointer; b: Builder; width, height: cfloat) {.cdecl.} =
let c = cast[ptr Calc](ctx)
# Size the keys from the space actually on offer, so the keypad fills the
# window however the compositor decided to size it. Length::Fill does not
# widen a cosmic button, so the host does the arithmetic instead.
let
gap = space(SpaceXs)
pad = space(SpaceM)
display = CaptionH + ValueH
# A dimension that is not a sane finite number means the library could not
# say, so fall back to the size the config asked for rather than dividing
# by infinity and collapsing every key to its floor.
availW = if float(width) > 0 and float(width) < 1.0e5: float(width) else: 420.0
availH = if float(height) > 0 and float(height) < 1.0e5: float(height) else: 640.0
keyW = max(MinKeyW, (availW - 2 * pad - float(Cols - 1) * gap) / float(Cols))
keyH = max(MinKeyH,
(availH - display - 2 * pad - float(Rows - 1) * gap) / float(Rows))
b.container:
b.fill()
b.alignCenter()
b.spacing(space(SpaceM))
# Always emitted, empty when there is nothing pending: the row has to
# occupy its space either way or the whole keypad shifts.
b.size(-1.0, CaptionH)
b.text(
if c[].failed: c[].reason
elif c.pending != opNone: &"{format(c.acc)} {c.pending}"
else: "",
TextCaption)
b.size(-1.0, ValueH)
b.text(c[].display, TextTitle1)
b.column:
b.spacing(gap)
for keyRow in Keypad:
b.row:
b.spacing(gap)
b.alignCenter()
for key in keyRow:
b.size(keyW, keyH)
if key.label.len > 0:
b.button(key.label, key.id, key.style)
else:
b.space(keyW, keyH) # hold the column open
proc main() =
var calc = newCalc()
var config = CosmicConfig(
title: "Nim ❤ COSMIC",
onView: onView,
onPress: onPress,
onKey: onKey,
ctx: addr calc,
width: 420,
height: 640,
)
let rc = cosmicRun(addr config)
if rc != 0:
quit(&"cosmic_run failed: {rc}", 1)
echo &"final display: {calc.display}"
when isMainModule:
main()
|