Back to Blog

Calculator App in Rust egui — Grid Layout & Operator Logic | Learn egui Ep28

Celest KimCelest Kim

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.