Recreating the Apple Calculator in Rust using Tauri, Yew and Tailwind
Tags: rust, tutorial, programming, learning
Published: 2023-11-30 22:05:40

<img width="100%" style="width:100%" src="https://raw.githubusercontent.com/max-taylor/Full-stack-Rust---Apple-Calculator/main/example/demo_video.gif">

## Introduction

In this tutorial, we will be rebuilding the Apple calculator using Rust. This project is designed to be a stimulating challenge, providing a hands-on experience with several key technologies:

- Tauri: An innovative framework for building lightweight desktop applications.
- Yew: A modern Rust framework for creating frontend web apps.
- TailwindCSS: A utility-first CSS framework for rapid UI development.

At a high-level the Apple calculator can be broken down into some key areas of functionality:

- Numeric Buttons (0-9)
- Operational Buttons (Add, Subtract, Multiply, Divide, Equals)
- Special Function Buttons: Percentage, Invert Sign, Decimal Point, Clear

We will be working through these step-by-step in this tutorial, creating reusable components where possible. [Here is the source code](https://github.com/max-taylor/Full-stack-Rust---Apple-Calculator)

Additionally, to complement this guide, I have prepared YouTube video demonstrating the entire build process. You can watch it here:

{% embed https://www.youtube.com/watch?v=sl0rDttMrIc&t=2s %}

And if you want to see more of my work, feel free to explore these links: [Twitter](https://twitter.com/max_t_dev) [YouTube](https://www.youtube.com/channel/UCNnu7xjW0hyUgRxOegrS86A)

Let's get started!

## Setup

Run the following command in your console:
```
yarn create tauri-app
```

Answer the prompts as follows:

- **Project name**: `calculator-app`
- **Frontend language**: Rust (cargo)
- **UI template**: Yew - (https://yew.rs/)


**Setup tailwind**

Integrating Tailwind CSS into our project will give us a powerful yet simple way to style our components. First, ensure you have `npx` installed, a tool that allows executing npm package binaries. It's a part of npm, and you can learn more about it [here](https://www.npmjs.com/package/npx).

To create a Tailwind configuration file, run:

```shell
npx tailwindcss init
```

This command generates a `tailwind.config.js` file in your project's root directory. Modify this file to include our Rust files and also add the colors we need for our button backgrounds:

```javascript
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./src/**/*.rs"],
  theme: {
    extend: {
      colors: {
        background: "#172425",
        darkGrey: "#2d3131",
        lightGrey: "#4d5250",
        orange: "#fd8d18",
      },
    },
  },
  plugins: [],
};
```

Next, create a CSS file named `src/input.css` and insert the following Tailwind directives:

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

To integrate this stylesheet into your app, update the `index.html` file in the root directory:

```html
<link href="output.css" data-trunk rel="css" />
```

This ensures that the generated CSS is included in the app.

**Title bar style:**

The Apple calculator doesn't have the title bar normal programs do, let's make some modifications to our Tauri config to fit that style:
```json
{
.  ...
  "tauri": {
	...
	
    "windows": [
      {
        "titleBarStyle": "Transparent",
        "title": ""
      }
    ]
  }
}
```

---

## Setting up the calculator

Let's first setup the states we need for our calculator, these are the main states we will need:
- `temp_value`: Tracks the current input from the user.
- `memory`: Stores the ongoing calculation or result.
- `action`: Holds the current action (like Add, Subtract).
- `trailing_dot`: If the decimal point was the last input with no subsequent digits.
- `has_dot_pressed`: If the `temp_value` has a decimal point

Modify `src/app.rs` to include the states:

```rust
#[function_component(App)]
pub fn app() -> Html {
    let temp_value: UseStateHandle<Option<f32>> = use_state(|| None);
    let memory: UseStateHandle<f32> = use_state(|| 0.0);
    let action: UseStateHandle<Option<Action>> = use_state(|| None);
    let trailing_dot: UseStateHandle<bool> = use_state(|| false);
    let has_dot_pressed: UseStateHandle<bool> = use_state(|| false);
}
```

Let's also add a display for the current calculator value, showing `temp_value` if it exists, otherwise `memory`:

```rust
let display_value: f32 = {
	if let Some(i) = *temp_value {
		i
	} else {
		*memory
	}
};

html! {
	<div class={classes!("h-screen", "w-screen", "bg-background", "flex-col", "flex")}>
        <div class={classes!("flex", "justify-end", "text-5xl", "pr-2", "py-1")}>
			{display_value}{match *trailing_dot {
			  true => ".",
			  false => "",
			}}
		</div>
	</div>
}
```

🐣 We now have the very basic beginnings of our calculator:

![Calculator beginnings](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kvpilcto4b0l77atlno6.png)

Let's continue giving it life!

**Defining Actions and Operations**

Within `src/app.rs` let's create an enum to represent calculator operations, this enum will be used for basic arithmetic. Let's call this enum `Action` and add a way to caste it to a string, so that we can easily display it in our UI later on:

```rust
#[derive(Clone, Copy, PartialEq)]
pub enum Action {
    Add,
    Subtract,
    Multiply,
    Divide,
    Equals,
}

impl Action {
    pub fn to_string(&self) -> String {
        match self {
            Action::Add => "+".to_string(),
            Action::Subtract => "-".to_string(),
            Action::Multiply => "x".to_string(),
            Action::Divide => "÷".to_string(),
            Action::Equals => "=".to_string(),
        }
    }
}
```

---

## Button Components

For a unified aesthetic across our calculator, create `src/buttons/button_styles.rs`. This file will contain the common styles for our buttons, ensuring consistency and ease of maintenance:

```rust
use yew::{classes, Classes};

pub fn get_button_styles(extra_classes: Classes) -> Classes {
    classes!(vec![
        classes!(
            "flex",
            "flex-1",
            "items-center",
            "justify-center",
            "rounded-none",
            "border-[0.25px]",
            "border-black",
            "text-xl",
            "font-bold",
            "hover:border-black" // override base styles
        ),
        extra_classes
    ])
}
```


Now, let's develop two primary button components: `NumberButton` for digits and `ActionButton` for operations.

**Number button:**

This component handles digit inputs (0-9). It takes a floating-point value, displays it, and invokes the specified callback function upon interaction:

```rust
use yew::{classes, function_component, html, Callback, Html, Properties};

use crate::buttons::button_styles::get_button_styles;

#[derive(Clone, PartialEq, Properties)]
pub struct Props {
    pub value: f32,
    pub onclick: Callback<f32>,
}

#[function_component]
pub fn NumberButton(Props { value, onclick }: &Props) -> Html {
    let value = *value;

    html!(
      <button class={get_button_styles(classes!("bg-lightGrey", "active:bg-[rgb(180,180,180)]"))} onclick={onclick.reform(move |_| value)}>
        {value}
      </button>
    )
}
```

**Action button:**

This component, slightly more complex, handles operational inputs like Add, Subtract, etc. It displays the provided action and communicates it back when the button is pressed. Additionally, it changes its style based on the whether this action button is currently the selected action:

```rust
use yew::{classes, function_component, html, Callback, Html, Properties};

use crate::{app::Action, buttons::button_styles::get_button_styles};

#[derive(Clone, PartialEq, Properties)]
pub struct Props {
    pub action: Action,
    pub selected_action: Option<Action>,
    pub onclick: Callback<Action>,
}

#[function_component]
pub fn ActionButton(
    Props {
        action,
        selected_action,
        onclick,
    }: &Props,
) -> Html {
    let action = *action;

    let is_selected = {
        if let Some(selected_action) = selected_action {
            *selected_action == action
        } else {
            false
        }
    };

    let classes = {
        if is_selected {
            classes!(
                "border-[1.2px]",
                "border-black",
                "h-full",
                "w-full",
                "flex",
                "items-center",
                "justify-center"
            )
        } else {
            classes!("")
        }
    };

    html!(
      <button class={get_button_styles(classes!("bg-orange", "active:bg-[rgb(184,115,51)]"))} onclick={onclick.reform(move |_| action)}>
          <div class={classes}>{action.to_string()}</div>
      </button>
    )
}
```

To make these button components accessible in our main app, we create `src/buttons.rs`:

```rust
pub mod action_button;
pub mod button_styles;
pub mod number_button;
```

Then, import this module in `src/main.rs` for seamless integration:

```rust
mod buttons;
```

---

## Calculator top row

First, let's define the top row of our calculator's interface, update your html in `src/app.rs` to be:

```rust
html! {
  <div class={classes!("h-screen", "w-screen", "bg-background", "flex-col", "flex")}>
	<div class={classes!("flex", "justify-end", "text-5xl", "pr-2", "py-1")}>
	  {display_value}{match *trailing_dot {
		true => ".",
		false => "",
	  }}
	</div>
	<div class={classes!("flex", "flex-1", "flex-row")}>
	  <button onclick={clear_state} class={get_button_styles(classes!("bg-darkGrey","active:bg-lightGrey"))}>
		{match clear_all {
		  true => "AC",
		  false => "C",
		}}
	  </button>
	  <button onclick={invert_sign} class={get_button_styles(classes!("bg-darkGrey","active:bg-lightGrey"))}>
		{"±"}
	  </button>
	  <button onclick={apply_percentage} class={get_button_styles(classes!("bg-darkGrey","active:bg-lightGrey"))}>
		{"%"}
	  </button>
	  <ActionButton action={Action::Divide} onclick={handle_action_button.clone()} selected_action={*action}  />
	</div>
  </div>
}
```

We have some missing functionality required to support these new buttons:

- **clear_state** - Clearing
- **invert_sign** - Inverting the sign (- to +)
- **apply_percentage** - Applying a percentage to the value
- **handle_action_button** - Handling actions

Let's walk through each of these

**Clearing:**

There are 2 distinct types of state clearing; CE (Clear everything) or C (Clear), this is how most calculators function and you'll notice on the Apple calculator that the clear button text changes depending on this state. When we clear everything, the major difference is that `memory` is also cleared, we only want to do this if; there is no temporary value, current action or a trailing dot. Let's implement this:

```rust
let clear_all = {
	if (*temp_value).is_some() || (*action).is_some() || *trailing_dot {
		false
	} else {
		true
	}
};

let clear_state = {
	let temp_value = temp_value.clone();
	let action = action.clone();
	let memory = memory.clone();
	let trailing_dot = trailing_dot.clone();
	let has_dot_pressed = has_dot_pressed.clone();

	Callback::from(move |_| {
		if clear_all {
			memory.set(0.0);
			temp_value.set(None);
			action.set(None);
		} else {
			action.set(None);
			temp_value.set(None);
		}

		trailing_dot.set(false);
		has_dot_pressed.set(false);
	})
};
```


**Inverting the sign & applying the percentage:**

The methods `invert_sign` and `apply_percentage` are similar; they either modify `temp_value` if it exists, otherwise they modify `memory`:

```rust
let invert_sign = {
	let temp_value = temp_value.clone();
	let memory = memory.clone();

	Callback::from(move |_| {
		if let Some(i) = *temp_value {
			temp_value.set(Some(i * -1.0));
		} else {
			memory.set(*memory * -1.0);
		}
	})
};

let apply_percentage = {
	let temp_value = temp_value.clone();
	let memory = memory.clone();

	Callback::from(move |_| {
		if let Some(i) = *temp_value {
			temp_value.set(Some(i / 100.0));
		} else {
			memory.set(*memory / 100.0);
		}
	})
};
```

**Handle the action button:**

This is a core piece of our functionality; handling actions. This essentially works by applying actions to our temporary value and memory to get our new result.

You'll notice this method applies the previous action that was pressed, not the new action. This is because when you press an action button on the calculator it doesn't immediately apply it, you then need to type in a temporary value to operate against. This action is processed when the next action button is pressed, typically this is equals but it also natively supports chaining calculations:

```rust
let handle_action_button = {
	let temp_value = temp_value.clone();
	let memory = memory.clone();
	let action = action.clone();
	let trailing_dot = trailing_dot.clone();
	let has_dot_pressed = has_dot_pressed.clone();

	Callback::from(move |passed_action: Action| {
		// If there is an action already pressed and a temporary_value, then we want to apply that previous action
		if let (Some(existing_action), Some(temp_value)) = (*action, *temp_value) {
			match existing_action {
				Action::Add => {
					memory.set(*memory + temp_value);
				}
				Action::Subtract => {
					memory.set(*memory - temp_value);
				}
				Action::Multiply => {
					memory.set(*memory * temp_value);
				}
				Action::Divide => {
					memory.set(*memory / temp_value);
				}
				Action::Equals => {
					memory.set(temp_value);
				}
			}
		} else {
			let memory_update = match *temp_value {
				Some(i) => i,
				None => *memory,
			};

			memory.set(memory_update);
		}
		if passed_action != Action::Equals {
			action.set(Some(passed_action));
		} else {
			action.set(None);
		}

		// Reset our temporary states
		temp_value.set(None);
		trailing_dot.set(false);
		has_dot_pressed.set(false);
	})
};
```

We will now have a pretty sleak, but somewhat useless calculator:

![Calculator with first row](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xm9mmmvrlg87qej8pv5p.png)


## Major button rows

Next, we'll add 3 additional rows, which will support 1-9 number buttons and 3 additional actions: Multiply, Subtract and Add. Insert the following code into the HTML macro in `src/app.rs`, positioning it below the previously created top row:

```rust
  <div class={classes!("flex", "flex-row", "flex-1")}>
	<NumberButton value={7.0}
	  onclick={handle_number_button_press.clone()} />
	<NumberButton value={8.0}
	  onclick={handle_number_button_press.clone()} />
	<NumberButton value={9.0}
	  onclick={handle_number_button_press.clone()} />
	<ActionButton action={Action::Multiply}
	  onclick={handle_action_button_press.clone()} selected_action={*action} />
  </div>
  <div class={classes!("flex", "flex-row", "flex-1")}>
	<NumberButton value={4.0}
	  onclick={handle_number_button_press.clone()} />
	<NumberButton value={5.0}
	  onclick={handle_number_button_press.clone()} />
	<NumberButton value={6.0}
	  onclick={handle_number_button_press.clone()} />
	<ActionButton action={Action::Subtract}
	  onclick={handle_action_button_press.clone()} selected_action={*action} />
  </div>
  <div class={classes!("flex", "flex-row", "flex-1")}>
	<NumberButton value={1.0}
	  onclick={handle_number_button_press.clone()} />
	<NumberButton value={2.0}
	  onclick={handle_number_button_press.clone()} />
	<NumberButton value={3.0}
	  onclick={handle_number_button_press.clone()} />
	<ActionButton action={Action::Add}
	  onclick={handle_action_button_press.clone()} selected_action={*action} />
  </div>
```

We have most of the functionality already to support this, the only missing part is the functionality required to support number buttons. To do this, we need to handle appending numbers to `temp_value`, and also managing decimal points:

```rust
let handle_number_button = {
	let temp_value = temp_value.clone();
	let trailing_dot = trailing_dot.clone();
	let has_dot_pressed = has_dot_pressed.clone();

	Callback::from(move |value: f32| {
		if let Some(i) = *temp_value {
			let trailing_dot_string = match *trailing_dot {
				true => ".",
				false => "",
			};

			let new_value = format!("{}{}{}", i, trailing_dot_string, value);

			temp_value.set(Some(new_value.parse::<f32>().unwrap()));
		} else {
			if *trailing_dot {
				let value = format!("0.{}", value).parse::<f32>().unwrap();
				temp_value.set(Some(value));
			} else {
				temp_value.set(Some(value));
			}
		}

		if *trailing_dot {
			trailing_dot.set(false);
			has_dot_pressed.set(true);
		}
	})
};
```

Our calculator will now look like this and be almost complete:


![Calculator almost complete, missing last row](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4b0trklc3avkb8kepz0w.png)

## Final row

Finally, we add the bottom row to our calculator, completing the UI:

```rust
<div class={classes!("flex", "flex-1", "flex-row")}>
	<div class={classes!("flex", "flex-1")}>
		<NumberButton value={0.0} onclick={handle_number_button.clone()} />
	</div>
	<div class={classes!("flex", "flex-1")}>
		<button onclick={handle_dot_press} class={get_button_styles(classes!("bg-lightGrey"))}>
			{"."}
		</button>
		<ActionButton action={Action::Equals} onclick={handle_action_button.clone()} selected_action={*action}  />
	</div>
</div>
```

Great, the only missing functionality for this is `handle_dot_press`, this function ensures that only one decimal point can be included in a number, ensuring correctness:

```rust
let handle_dot_press = {
	let trailing_dot = trailing_dot.clone();
	let has_dot_pressed = has_dot_pressed.clone();

	Callback::from(move |_| {
		if *has_dot_pressed {
			return;
		}

		trailing_dot.set(true);
	})
};
```

---
## Done!

🥳 Congratulations! We've successfully built a fully functional calculator that mimics the Apple calculator's design and functionality.

![Calculator complete](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l9aw1usocrjc1p83yruk.png)

I hope you found this tutorial insightful and engaging. For more content and updates, connect with me on [Twitter](https://twitter.com/max_t_dev) and [YouTube](https://www.youtube.com/channel/UCNnu7xjW0hyUgRxOegrS86A).