Calculator App in Rust egui — Grid Layout & Operator Logic | Learn egui Ep28
Video: Calculator App in Rust egui — Grid Layout & Operator Logic | Learn egui Ep28 by Taught by Celeste AI - AI Coding Coach
Watch full page →Calculator App in Rust egui — Grid Layout & Operator Logic
In this tutorial, we build a functional calculator app using Rust and the egui GUI framework. The app features a dark-themed display panel, a color-coded button grid with orange operators and gray numbers, and an operator state machine that supports chained arithmetic, decimals, percentages, negation, and handles division-by-zero errors gracefully.
Code
use eframe::{egui, epi};
use egui::{Color32, FontId, Layout, RichText};
// Define calculator operators
#[derive(PartialEq)]
enum Operator {
Add,
Subtract,
Multiply,
Divide,
None,
}
struct Calculator {
display: String,
first_operand: Option,
operator: Operator,
reset_next: bool,
error: bool,
}
impl Default for Calculator {
fn default() -> Self {
Self {
display: "0".to_owned(),
first_operand: None,
operator: Operator::None,
reset_next: false,
error: false,
}
}
}
impl Calculator {
// Helper to append digits or decimal point
fn press_digit(&mut self, digit: char) {
if self.reset_next || self.error {
self.display.clear();
self.reset_next = false;
self.error = false;
}
if digit == '.' && self.display.contains('.') {
return; // Prevent multiple decimals
}
if self.display == "0" && digit != '.' {
self.display.clear();
}
self.display.push(digit);
}
// Helper to handle operator button press
fn press_operator(&mut self, op: Operator) {
if self.error {
return;
}
if let Some(first) = self.first_operand {
if !self.reset_next {
if let Ok(second) = self.display.parse::() {
match self.evaluate(first, second, &self.operator) {
Some(result) => {
self.display = result.to_string();
self.first_operand = Some(result);
}
None => {
self.display = "Error".to_owned();
self.error = true;
self.first_operand = None;
self.operator = Operator::None;
return;
}
}
}
}
} else {
if let Ok(value) = self.display.parse::() {
self.first_operand = Some(value);
}
}
self.operator = op;
self.reset_next = true;
}
// Evaluate arithmetic based on operator
fn evaluate(&self, a: f64, b: f64, op: &Operator) -> Option {
match op {
Operator::Add => Some(a + b),
Operator::Subtract => Some(a - b),
Operator::Multiply => Some(a * b),
Operator::Divide => if b == 0.0 { None } else { Some(a / b) },
Operator::None => Some(b),
}
}
// Clear calculator state
fn clear(&mut self) {
self.display = "0".to_owned();
self.first_operand = None;
self.operator = Operator::None;
self.reset_next = false;
self.error = false;
}
}
impl epi::App for Calculator {
fn name(&self) -> &str {
"egui Calculator"
}
fn update(&mut self, ctx: &egui::Context, _: &epi::Frame) {
egui::TopBottomPanel::top("display_panel")
.frame(egui::Frame {
fill: Color32::from_gray(30),
corner_radius: 8.0,
..Default::default()
})
.show(ctx, |ui| {
ui.with_layout(Layout::right_to_left(egui::Align::Center), |ui| {
ui.label(
RichText::new(&self.display)
.font(FontId::monospace(40.0))
.color(Color32::WHITE),
);
});
});
egui::CentralPanel::default().show(ctx, |ui| {
let button_size = egui::Vec2::new(60.0, 60.0);
let operator_color = Color32::from_rgb(255, 165, 0); // Orange
let number_color = Color32::from_gray(100);
// Helper to create colored buttons
let mut button = |label: &str, color: Color32| {
ui.add_sized(button_size, egui::Button::new(label).fill(color))
};
// Row 1: Clear, Negate, Percent, Divide
ui.horizontal(|ui| {
if button("C", operator_color).clicked() {
self.clear();
}
if button("±", operator_color).clicked() {
if let Ok(value) = self.display.parse::() {
self.display = (-value).to_string();
}
}
if button("%", operator_color).clicked() {
if let Ok(value) = self.display.parse::() {
self.display = (value / 100.0).to_string();
}
}
if button("÷", operator_color).clicked() {
self.press_operator(Operator::Divide);
}
});
// Row 2: 7,8,9, Multiply
ui.horizontal(|ui| {
for &digit in &['7', '8', '9'] {
if button(&digit.to_string(), number_color).clicked() {
self.press_digit(digit);
}
}
if button("×", operator_color).clicked() {
self.press_operator(Operator::Multiply);
}
});
// Row 3: 4,5,6, Subtract
ui.horizontal(|ui| {
for &digit in &['4', '5', '6'] {
if button(&digit.to_string(), number_color).clicked() {
self.press_digit(digit);
}
}
if button("−", operator_color).clicked() {
self.press_operator(Operator::Subtract);
}
});
// Row 4: 1,2,3, Add
ui.horizontal(|ui| {
for &digit in &['1', '2', '3'] {
if button(&digit.to_string(), number_color).clicked() {
self.press_digit(digit);
}
}
if button("+", operator_color).clicked() {
self.press_operator(Operator::Add);
}
});
// Row 5: 0 (wide), ., Equals
ui.horizontal(|ui| {
if ui
.add_sized(
egui::Vec2::new(button_size.x * 2.0 + 10.0, button_size.y),
egui::Button::new("0").fill(number_color),
)
.clicked()
{
self.press_digit('0');
}
if button(".", number_color).clicked() {
self.press_digit('.');
}
if button("=", operator_color).clicked() {
if let Some(first) = self.first_operand {
if let Ok(second) = self.display.parse::() {
match self.evaluate(first, second, &self.operator) {
Some(result) => {
self.display = result.to_string();
self.first_operand = None;
self.operator = Operator::None;
self.reset_next = true;
}
None => {
self.display = "Error".to_owned();
self.error = true;
self.first_operand = None;
self.operator = Operator::None;
}
}
}
}
}
});
});
}
}
Key Points
- Use egui's TopBottomPanel with a custom Frame to create a styled, fixed calculator display panel.
- Color-code buttons with Button::new().fill(Color32) to visually distinguish operators and numbers.
- Implement an operator state machine to handle chained arithmetic and reset display after operations.
- Parse the display string to f64 for arithmetic and carefully handle division by zero by showing an error.
- Use horizontal layouts with add_sized to build a consistent button grid, including a wide zero button spanning two columns.