nandi/cosmicnimpublic Fork 0
v0.4.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 · 199 lines · 5.5 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
## 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
  # Keys fill their row, so the four columns split the width evenly and grow
  # with the window instead of each key hugging its own label.
  KeyH = 84.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

  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 IdPercent:
    if c.entry.len > 0: c.entry = format(parseFloat(c.entry) / 100.0)
    else: c.acc = c.acc / 100.0
  else:
    discard

proc onView(ctx: pointer; b: Builder) {.cdecl.} =
  let c = cast[ptr Calc](ctx)

  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.fill()
      b.spacing(space(SpaceXs))
      for keyRow in Keypad:
        b.row:
          b.spacing(space(SpaceXs))
          b.alignCenter()
          b.fill()
          for key in keyRow:
            b.size(Fill, KeyH)
            if key.label.len > 0:
              b.button(key.label, key.id, key.style)
            else:
              b.space(0, KeyH)   # hold the column open

proc main() =
  var calc = newCalc()
  var config = CosmicConfig(
    title: "Nim ❤ COSMIC",
    onView: onView,
    onPress: onPress,
    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()