 Published on

# How to build a Calculator App using Rust and Iced GUI Library  Ever wondered how to create a calculator app using Rust and the Iced GUI library? Well look no further because we go over how to do in this blog post.

Before we start build this calculator application here are some GIFs of it in action:    Figure 2: Multiplication operation  Figure 3: Division operation  Figure 4: Sin operation  Figure 5: Factorial operation  Figure 6: DEL and CE operation

## Source Code

The source code for this project can be found in this GitHub repo

Feel free to clone it locally and follow along or make whatever additions to it you feel fits.

## Development

To start off let's have a high level overview of the backend calculator logic.

Starting with the `src/parser/ast.rs` file we can see the following code:

``````extern crate std;
use std::collections::HashMap;
use std::f64;

pub trait Node {
fn eval(&self, env: &mut HashMap<String, f64>) -> Option<f64>;
}

pub struct Num {
pub num: f64
}

impl Node for Num {
fn eval(&self, _env: &mut HashMap<String, f64>) -> Option<f64> {
Some(self.num)
}
}

pub left: Box<dyn Node>,
pub right: Box<dyn Node>,
}

fn eval(&self, env: &mut HashMap<String, f64>) -> Option<f64> {
match self.left.eval(env) {
Some(l) => {
match self.right.eval(env) {
Some(r) => Some(l + r),
None => None
}
}
None => None
}
}
}

...

``````

Here is an overview of the above code:

• `pub struct Num`: This is the most basic struct that is used extensively throughout our application. It represents a number using its field `num`, a `f64` primitive.
• `pub trait Node`: This is the most basic trait used in the application and it allows the structs that implement it to use the `eval()` function that it abstracts.
• `pub struct Add`: This struct represents the addition operator. It implements the `Node` trait which allows us to use the `eval()` method. For this specific use-case the left `f64` variable is added to the right `f64` variable via the expression `Some(r) => Some(l + r)` and the resulting `f64` variable is returned as the output

Similar to the `Add` struct here is the `Mul` struct that handles multiplication operations:

``````...
pub struct Mul {
pub left: Box<dyn Node>,
pub right: Box<dyn Node>,
}

impl Node for Mul {
fn eval(&self, env: &mut HashMap<String, f64>) -> Option<f64> {
match self.left.eval(env) {
Some(l) => {
match self.right.eval(env) {
Some(r) => Some(l * r),
None => None
}
}
None => None
}
}
}
...
``````

The `Mul` struct returns `Some(l * r)` in order to implement the multiplication functionality for our calculator.

Next, let's cover an operational struct, the `Pow` struct:

``````...
pub struct Pow {
pub base: Box<dyn Node>,
pub exponent: Box<dyn Node>
}

impl Node for Pow {
fn eval(&self, env: &mut HashMap<String, f64>) -> Option<f64> {
match self.base.eval(env) {
Some(b) => {
match self.exponent.eval(env) {
Some(e) => Some(b.powf(e)),
None => None
}
}
None => None
}
}
}
...
``````

Here we're using the `powf()` function to raise the base `f64` number to the power specified exponent number.

Lastly let's see how a trigonometric operation is handled:

``````...
pub struct Cos {
pub arg: Box<dyn Node>
}

impl Node for Cos {
fn eval(&self, env: &mut HashMap<String, f64>) -> Option<f64> {
match self.arg.eval(env) {
Some(x) => Some(x.cos()),
None => None
}
}
}
...
``````

Here we're calculating the Cosine of the variable `arg` using the `cos` function

Next let's move on to the `lexer.rs` file and cover the most important parts:

``````
pub struct Lexer {
pub curr:  char,
pub pos: usize,
pub src: String,
pub eof: bool
}

impl Lexer {
pub fn new(src: &str) -> Lexer {
let mut l = Lexer {
curr: '\0',
pos: 0,
src: src.to_string(),
eof: false
};
if l.pos >= src.len() {
l.eof = true;
} else {
l.curr = src.chars().nth(0).unwrap();
}
l
}
pub fn next_token(&mut self) -> Result<token::Token, String> {
if self.eof {
return Ok(EOF);
}
self.consume_whitespace();
match self.curr {

'(' => {self.bump(); Ok(LPAREN)}
')' => {self.bump(); Ok(RPAREN)}
c if c.is_digit(10) => {
let start = self.pos;
let mut end = start + 1;
self.bump();
while (self.curr.is_digit(10) || self.curr == '.') && !self.eof{
self.bump();
end += 1;
}
Ok(NUMBER(self.src[start..end].parse::<f64>().unwrap()))
}

c if c.is_alphabetic() => {
let start = self.pos;
let mut end = start + 1;
self.bump();
while self.curr.is_alphabetic() && !self.eof {
self.bump();
end += 1;
}
Ok(SYMBOL(self.src[start..end].to_string()))
}
'-' => {self.bump(); Ok(SUB)}
'*' => {self.bump(); Ok(MUL)}
'/' => {self.bump(); Ok(DIV)}
'^' => {self.bump(); Ok(CARET)}
'=' => {self.bump(); Ok(EQUALS)}
'%' => {self.bump(); Ok(MOD)}
c => { Err(format!("unexpected token {} at position {}", c, self.pos)) }
}
}
pub fn bump(&mut self) {
self.pos += 1;
if self.pos >= self.src.len() {
self.eof = true;
return;
}
self.curr = self.src.chars().nth(self.pos).unwrap();
}

pub fn consume_whitespace(&mut self) {
while is_whitespace(self.curr) {
self.bump();
}
}
}
``````

In case you didn't know, a lexer is a software program that is responsible for extracting individual words from a stream of sentences.

In our case, this `lexer` struct is designed to extract the numbers and operation symbols from the input string provided from the GUI so that the necessary calculation can be performed. Here is a brief overview of the methods in the `Lexer` struct:

• `new()`: This constructor is used to create a new instance of the `Lexer` struct with some default values for the fields `curr`, `pos`, `src` and `eof`. These fields will hold information about the input string provided, such as the current character being processed, its position, and whether or not the end of the string has been reached.
• `next_token()`: This method is responsible for moving to the new token in the list of tokens created by deconstructing the input string. The next token is categorized into whether it is a number, math operation, parentheses or unexpected character. The category is then returned in the `Ok()` method
• `bump()`: This method increments the current position in the input string being read and assigns the new value to the field `self.curr`
• `consume_whitespace()`: This method just skips any whitespace in the input string and moves on to the next position

Next, let's move on to the `src/parser/token.rs` file and cover the most important sections:

``````...
pub enum Token {
LPAREN,
RPAREN,
SUB,
MUL,
DIV,
MOD,
CARET,
EQUALS,
NUMBER(f64),
SYMBOL(String),
EOF
}

impl Token {
/* returns (prec, associativity) where 0 is left and 1 is right*/
pub fn info(&self) -> Option<(usize, usize)> {
match *self {
ADD | SUB => Some((10, 0)),
MUL | DIV | MOD => Some((20, 0)),
CARET => Some((30, 1)),
_ => { None}
}
}

pub fn to_char(&self) -> char {
match *self {
LPAREN => '(',
RPAREN => ')',
SUB => '-',
MUL => '*',
DIV => '/',
CARET => '^',
MOD => '%',
EQUALS => '=',
EOF => 'E',
NUMBER(_) => 'N',
SYMBOL(_) => 'S',
}
}
}
...
``````

Here is a quick overview of this file:

• `pub enum Token`: This enum contains all the different types of characters that can be considered tokens for this application. Notice that `SYMBOL(String)` and `NUMBER(f64)` are enum variants. `SYMBOL(String)` can take values like `Sin()`, `Tan()`, `fact()` and `NUMBER(f64)` takes on any number inputted into the calculator
• `fn info(&self)`: This method sets the order of operations for the calculation. For example, if we have the expression `4 + 5 / 6 + 4^4`, then caret operation will be handled first followed by the division operation and concluded with the addition operations.
• `fn to_char(&self)`: This method converts a `Token` instance to its character representation. For example: if the `Token::Add` is passed into it as an argument then the character `'+'` will be returned as output

Now, let's move onto the `src/parser/mod.rs` files and cover the most important sections:

``````
pub struct Parser {
pub current: token::Token,
pub lexer: lexer::Lexer,
pub peeked: Option<token::Token>,
}

impl Parser {
pub fn new(input: &str) -> Parser {
let l = lexer::Lexer::new(input);
let p = Parser {
current: EOF,
peeked: None,
lexer: l
};
p
}
...
}
``````

In the above code we can see that the `Parser` struct contains the following fields:

• `current`: This field is for representing the current token from the input string
• `lexer`: This is the `Lexer` instance that extracts tokens from the input string
• `peeked`: This is an `Option` field that may or may not contain a token. It is used to check whether there is a next token after the current token being processed by the application.

Then in the implementation block we declare a `new()` with a set of default values for the aforementioned fields.

Here are some important methods in `src/parser/mod.rs`:

``````    pub fn parse(&mut self) -> Result<Box<dyn ast::Node>, String> {
self.expr(1)
}

pub fn expr(&mut self, prec: usize) -> Result<Box<dyn ast::Node>, String> {
let mut lhs = self.atom()?;
let mut rhs;

loop {
let curr = self.peek_token()?;
if token::is_eof(&curr) {
//println!("breaking out of expr loop");
break;
}
...
Ok(lhs)
}

pub fn atom(&mut self) -> Result<Box<dyn ast::Node>, String> {
match self.peek_token()? {
EOF => { Ok(Box::new( ast::Num {num: 0f64})) }
LPAREN => {
self.expect('(')?;
let e = self.expr(1)?;
self.expect(')')?;
Ok(e)
}
...
}

pub fn op (&self, op: token::Token, lhs: Box<dyn ast::Node>, rhs: Box<dyn ast::Node>)
-> Box<dyn ast::Node> {
match op {
left: lhs,
right: rhs
})
}
...
}
}

pub fn function<'a>(&'a self, op: String, arg: Box<dyn ast::Node>) -> Box<dyn ast::Node> {
match &op[..] {
"sin" | "sine" => {
Box::new( ast::Sin {
arg: arg
})
}
...
}
``````

And here is a brief overview of the above methods:

• `fn parser()`: This method is used to start the parsing process once the input string to be calculated has been received from the Calculator GUI
• `fn expr()`: This recursive method uses a while loop to identify the left-hand side and right-hand side numbers along with the operation symbols in the middle of them. After identification, the left-hand side value is saved in the `lhs` variable, right-hand side value is saved in the `rhs` variable and the operation to be performed is saved in `curr`. Finally, those variables are passed to the `op()` method for evaluation and the output is returned as a `Result<Box<dyn ast::Node>, String>` data type. That is, if the evaluation was successful a `Box<dyn ast::Node>` is returned and if not then an error `String` is returned.
• `fn atom()`: This method used to check the next token in the input string using the `peek_token()` method and the return a `Box<dyn ast::Node>` type instance that matches the type of the token that was peeked. An error `String` is returned if the end of the input string has been reached.
• `fn op()`: This method evaluates the left-hand side and right-hand side values on the basis of the `op` argument that is passed into it. The evaluation is done using the `ast` module's operation structs shown above.
• `fn function()`: This method is quite similar to `op()` except that it handles single number operations such as `sin`, `sqrt`, `fact` and `tan`.

And now here are the remaining methods in the `Parser` struct:

``````    pub fn expect(&mut self, tok: char) -> Result<(), String> {
self.next_token()?;
if self.current.to_char() != tok {
return Err(format!("expected {:?} but found {}", tok, self.current.to_char()));
}
Ok(())
}
pub fn peek_token(&mut self) -> Result<token::Token, String> {
if self.peeked.is_none() {
self.peeked = Some(self.lexer.next_token()?);
}
Ok(self.peeked.clone().unwrap())
}
pub fn next_token(&mut self) -> Result<(), String> {
match self.peeked {
Some(ref mut pk) => {
self.current = pk.clone();
}
None => {
self.current = self.lexer.next_token()?;
}
}
self.peeked = None;
Ok(())
}

``````

Along with their explanations:

• `fn expect()`: This method is used to predict the next token that comes after the current token. The `next_token()` method is called to increment the parser to the next token and then the expected value is compared to the actual new token value. Finally, it returns nothing if the expectation is fulfilled or a error string if it is not
• `fn peek_token()`: This method is used to find out the next token in the input string without actually making it the new current token. Doing so allows the parser to construct mathematical expressions out of the list of tokens generated by the lexer
• `fn next_token()`: This method is used to obtain the next token in the list of tokens generated by the lexer

Now let's cover the final file to be covered, which is the `src/main.rs` file:

``````...
pub struct Calculator {
pub input_string: String,
pub output_string: String,
}

impl Calculator {
pub async fn calculate(
input_string: String,
) -> String {
let result_output = Self::evaluate_expr(&input_string).await;
match result_output {
Ok(result) => result.to_string(),
Err(result) => result
}
}

async fn evaluate(input: &str, env: &mut HashMap<String, f64>) -> Result<f64, String> {
let mut p = parser::Parser::new(input);
let ast = p.parse()?;
match ast.eval(env) {
Some(result) => Ok(result),
None => Err("No value for that expression!".to_string())
}
}

async fn evaluate_expr(input_string: &str) -> Result<f64, String> {
use std::f64;
let mut env = HashMap::new();
env.insert("wow".to_string(), 35.0f64);
env.insert("pi".to_string(), f64::consts::PI);

let mut input = input_string;

let expression_text = input.trim_right();

let result = Self::evaluate(expression_text, &mut env);
match result.await {
Ok(value) => {
Ok(value)
}
Err(s) => {
Err(s)
}
}
}

}
``````

Here is an explanation for the above code:

• `pub struct Calculator`: This is the struct that will start the process of calculation using the files we've covered in the `src/parser` folder. It contains the `input_string` field which contains the inputted string obtained from the GUI and the `output_string` string will be assigned the resulting value derived from the logic in the `src/parser` package.
• `pub async fn calculate()`: This is the method that will call the `evaluate_expr()` method in order to start the calculation process. The resulting value will be assigned to the `result_output` variable which is the output for this method
• `pub async fn evaluate_expr()`: This method will trim uneccesary whitespace from the input string before providing the input string to the `evaluate()` method
• `pub async fn evaluate()`: This method calls the `parse()` method in the `src/parser/mod.rs` file to start the parsing process

And now here is the constructor of the `CalculatorGUI` struct:

``````struct CalculatorGUI {
display_text: String,
done_calculation: bool,
}

impl Application for CalculatorGUI {
type Message = Message;
type Theme = Theme;
type Executor = executor::Default;
type Flags = ();

fn new(_flags: ()) -> (CalculatorGUI, Command<Message>) {
(
CalculatorGUI {
display_text: "".to_string(),
done_calculation: true,
},
Command::perform(Calculator::calculate("".to_string()), Message::DoneCalculating),
)
}

...

}
``````

The constructor sets the default text to be displayed in the display area to an empty string and the `done_calculation` variable to true. This variable is used to indicate when the provided input string's calculation has been completed. The `Command::perform()` call is used to create a `Command` instance that performs the action of the given future, which is the `Calculator::calculate()` method in this case. Note that is not using the `Command` in the `std::process` library but the one provided by Iced which you can see here

Next let's cover the `update()` method:

``````fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::One => {
if self.done_calculation {
self.display_text = "1".to_string();
self.done_calculation = false;
} else {
self.display_text += "1";
}

Command::none()
},
...
}
...
}
``````

This method used to handle a `Message` instance and update the state of the `Application`. For the sake of brevity here are the most important updates:

• Number type messages(Ex: `Message::Three`) adds the corresponding number character to the input string `self.display_text`
• Operation type messages(Ex: `Message::Multiply`) adds the corresponding operation symbol to the input string
• Calculation type messages(Ex: `Message::Equals`) calls the `Calculator::calculate()` method in order to start the calculation process
• `Message::DoneCalculating` is the message used to indicate that the result of the calculation has been completed and then the resulting value is shown in the display text area

Up next is the `view()` method which determines the widgets to be displayed in the `Application`. As this is a calculator-themed application all the widgets inside this method correspond to the various number buttons, operation buttons, `CE`, `DEL` and the display text area(which is represented by the widget `display_text`)

The final code snippet to explain is the following:

``````mod theme {
use iced::widget::{button, container, text, row};
use iced::{application, color, Color};

#[derive(Debug, Clone, Copy, Default)]
pub struct Theme;

impl application::StyleSheet for Theme {
type Style = ();

fn appearance(&self, _style: &Self::Style) -> application::Appearance {
application::Appearance {
background_color: color!(0x28, 0x28, 0x28),
text_color: color!(0xeb, 0xdb, 0xb2),
}
}
}

impl text::StyleSheet for Theme {
type Style = ();

fn appearance(&self, _style: Self::Style) -> text::Appearance {
text::Appearance {
color: color!(0xeb, 0xdb, 0xb2).into(),
}
}
}
...

}
``````

This `theme` module is a custom theme developed for this `Application`. It allows us to add a light blue border for the main `container` widget and the display text area widget, which is a `button` widget.

Well that's it for the development section!

## Demonstration:

In order to build and run the application run the following command in the application root directory:

``````cargo run --package basic-calculator
``````

Now here are some GIFs of the finished calculator application in action:    Figure 2: Multiplication operation  Figure 3: Division operation  Figure 4: Sin operation  Figure 5: Factorial operation  Figure 6: DEL and CE operation

## Conclusion

Thanks for reading this blog post!

If you have any questions or concerns please feel free to post a comment in this post and I will get back to you if I find the time.