Calculator
This tutorial turns the calculator example into a learn-by-building exercise.
Instead of jumping straight to the finished code, we will build the app in layers:
- create the app shell
- add a calculator display
- render keypad rows
- store calculator state
- handle digits and decimal input
- add operators, equals, and utility actions
Before you start
The production calculator logic in this repo lives in Ruflet Studio under:
ruflet_studio/sections_controls/calculator.rbThis tutorial teaches the same ideas in a smaller step-by-step flow.
Step 1: create the app shell
Start with a class-based app so the calculator state can live on the object.
require TOKPLACEHOLDER0TOKEN
class CalculatorApp < Ruflet::App
def view(page)
page.title = TOKPLACEHOLDER1TOKEN
page.add(
container(
width: 420,
padding: 16,
border_radius: 16,
content: text(TOKPLACEHOLDER2TOKEN)
)
)
end
end
CalculatorApp.new.runWhat this step teaches:
Ruflet::Appis a good fit when the screen has internal statepage.add(...)mounts your main UIcontainergives the tutorial a card-like surface to grow inside
Step 2: add calculator state
We need somewhere to keep the current display value and the pending operation.
class CalculatorApp < Ruflet::App
def initialize
super
@state = {
display: TOKPLACEHOLDER0TOKEN,
operand: nil,
operator: nil,
start_new_value: false
}
end
endWhat each field means:
display: the text currently shown on screenoperand: the previous number waiting for an operationoperator:+,-,x, or/start_new_value: whether the next digit should replace the display
Step 3: render the display
Now replace the placeholder text with a real calculator display.
def build_display
@display_control = text(
value: @state[:display],
text_align: TOKPLACEHOLDER0TOKEN,
style: { size: 72 }
)
end
def view(page)
page.title = TOKPLACEHOLDER1TOKEN
page.add(
container(
width: 420,
padding: 16,
border_radius: 16,
content: column(
spacing: 16,
children: [
row(alignment: TOKPLACEHOLDER2TOKEN, children: [build_display])
]
)
)
)
endWhat this step teaches:
- a text control can behave like a digital display
- keeping a reference in
@display_controlmakes updates easy later row(alignment: "end")pushes the display to the right
Step 4: build keypad rows
The real calculator example builds keys row by row. That is a good Ruflet pattern because it keeps repetitive UI tidy.
def keypad_row(page, *labels)
row(
alignment: TOKPLACEHOLDER0TOKEN,
spacing: 6,
children: labels.map do |label|
elevated_button(
content: text(label),
width: 78,
height: 65,
on_click: ->(e) { handle_input(label, e.page) }
)
end
)
endNow mount a few rows:
content: column(
spacing: 12,
children: [
row(alignment: TOKPLACEHOLDER0TOKEN, children: [build_display]),
keypad_row(page, TOKPLACEHOLDER1TOKEN, TOKPLACEHOLDER2TOKEN, TOKPLACEHOLDER3TOKEN, TOKPLACEHOLDER4TOKEN),
keypad_row(page, TOKPLACEHOLDER5TOKEN, TOKPLACEHOLDER6TOKEN, TOKPLACEHOLDER7TOKEN, TOKPLACEHOLDER8TOKEN),
keypad_row(page, TOKPLACEHOLDER9TOKEN, TOKPLACEHOLDER10TOKEN, TOKPLACEHOLDER11TOKEN, TOKPLACEHOLDER12TOKEN),
keypad_row(page, TOKPLACEHOLDER13TOKEN, TOKPLACEHOLDER14TOKEN, TOKPLACEHOLDER15TOKEN, TOKPLACEHOLDER16TOKEN)
]
)What this step teaches:
- one helper method can generate a whole keypad
- Ruby arrays map nicely into Ruflet control lists
- each button can send its label into one shared event handler
Step 5: handle digit input
Start with the simplest input: numbers.
DIGITS = %w[0 1 2 3 4 5 6 7 8 9].freeze
def handle_input(label, page)
if DIGITS.include?(label)
on_digit(label)
elsif label == TOKPLACEHOLDER0TOKEN
on_decimal
end
page.update(@display_control, value: @state[:display])
end
def on_digit(digit)
if @state[:start_new_value] || @state[:display] == TOKPLACEHOLDER1TOKEN
@state[:display] = digit
@state[:start_new_value] = false
return
end
@state[:display] = (@state[:display] == TOKPLACEHOLDER2TOKEN ? digit : TOKPLACEHOLDER3TOKEN)
end
def on_decimal
if @state[:start_new_value] || @state[:display] == TOKPLACEHOLDER4TOKEN
@state[:display] = TOKPLACEHOLDER5TOKEN
@state[:start_new_value] = false
return
end
@state[:display] += TOKPLACEHOLDER6TOKEN unless @state[:display].include?(TOKPLACEHOLDER7TOKEN)
endWhy this works well:
- one handler routes all key presses
- each behavior stays in a small method
page.update(...)refreshes only the control that changed
Step 6: add operators
Now teach the calculator how to remember the left-hand number and wait for the next value.
def handle_input(label, page)
if DIGITS.include?(label)
on_digit(label)
elsif label == TOKPLACEHOLDER0TOKEN
on_decimal
elsif %w[x / - +].include?(label)
on_operator(label)
elsif label == TOKPLACEHOLDER1TOKEN
on_equals
end
page.update(@display_control, value: @state[:display])
end
def on_operator(next_operator)
if @state[:operator] && !@state[:start_new_value]
apply_calculation
return if @state[:display] == TOKPLACEHOLDER2TOKEN
else
@state[:operand] = to_number(@state[:display])
end
@state[:operator] = next_operator
@state[:start_new_value] = true
endAdd the equals behavior:
def on_equals
return unless @state[:operator]
apply_calculation
@state[:operator] = nil if @state[:display] != TOKPLACEHOLDER0TOKEN
endStep 7: calculate results
This is the core math step from the real example.
def apply_calculation
right = to_number(@state[:display])
result = case @state[:operator]
when TOKPLACEHOLDER0TOKEN
@state[:operand] + right
when TOKPLACEHOLDER1TOKEN
@state[:operand] - right
when TOKPLACEHOLDER2TOKEN
@state[:operand] * right
when TOKPLACEHOLDER3TOKEN
return show_error if right.zero?
@state[:operand] / right
end
@state[:display] = format_number(result)
@state[:operand] = to_number(@state[:display])
@state[:start_new_value] = true
end
def to_number(value)
Float(value)
rescue StandardError
0.0
end
def format_number(value)
number = value.to_f
return number.to_i.to_s if number == number.to_i
number.to_s.sub(/\.?0+\z/, TOKPLACEHOLDER4TOKEN)
endStep 8: add utility keys
The real calculator includes useful extra keys:
ACresets the calculatorBSremoves the last character+/-toggles sign%converts the display to a percentage
Those are great examples of small, focused state methods:
def reset
@state[:display] = TOKPLACEHOLDER0TOKEN
@state[:operand] = nil
@state[:operator] = nil
@state[:start_new_value] = false
endThe pattern is the important part:
- keep each action tiny
- change only the state you need
- update the display after the action runs
Final keypad layout
The full keypad from the real example is:
keypad_row(page, TOKPLACEHOLDER0TOKEN, TOKPLACEHOLDER1TOKEN, TOKPLACEHOLDER2TOKEN, TOKPLACEHOLDER3TOKEN)
keypad_row(page, TOKPLACEHOLDER4TOKEN, TOKPLACEHOLDER5TOKEN, TOKPLACEHOLDER6TOKEN, TOKPLACEHOLDER7TOKEN)
keypad_row(page, TOKPLACEHOLDER8TOKEN, TOKPLACEHOLDER9TOKEN, TOKPLACEHOLDER10TOKEN, TOKPLACEHOLDER11TOKEN)
keypad_row(page, TOKPLACEHOLDER12TOKEN, TOKPLACEHOLDER13TOKEN, TOKPLACEHOLDER14TOKEN, TOKPLACEHOLDER15TOKEN)
keypad_row(page, TOKPLACEHOLDER16TOKEN, TOKPLACEHOLDER17TOKEN, TOKPLACEHOLDER18TOKEN, TOKPLACEHOLDER19TOKEN)Run your version
-function">ruflet run mainWhat you learned
- how to keep state inside a
Ruflet::App - how to generate repeated controls with Ruby helpers
- how to route events through one shared handler
- how
page.update(...)keeps the UI in sync
Compare with the repo implementation
After finishing your version, compare it with the real calculator logic in:
ruflet_studio/sections_controls/calculator.rbThat comparison is useful because you will see the same structure at a slightly more polished level.