nandi/cosmicnimpublic Fork 0
v0.6.0
Commits
Clone
git clone https://git.rickub.com/nandi/cosmicnim.git
git clone ssh://git@rickub.com/nandi/cosmicnim.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

calculator.nim · 246 lines · 7.1 KBNim Blame HistoryRaw
  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
## 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 = "÷"

  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: bool

const
  Cols = 4
  Rows = 5
  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 display*(c: Calc): string =
  if c.error: "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 = true
    else: c.acc = c.acc / rhs
  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.error:
    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.error:
      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 = 96.0
    # 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))

    # The pending operation only exists as a line when there is one.
    if c.pending != opNone and not c.error:
      b.text(&"{format(c.acc)} {c.pending}", TextCaption)

    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()