Cookie Consent by Free Privacy Policy Generator ๐Ÿ“Œ Recreating the Apple Calculator in Rust using Tauri, Yew and Tailwind

๐Ÿ  Team IT Security News

TSecurity.de ist eine Online-Plattform, die sich auf die Bereitstellung von Informationen,alle 15 Minuten neuste Nachrichten, Bildungsressourcen und Dienstleistungen rund um das Thema IT-Sicherheit spezialisiert hat.
Ob es sich um aktuelle Nachrichten, Fachartikel, Blogbeitrรคge, Webinare, Tutorials, oder Tipps & Tricks handelt, TSecurity.de bietet seinen Nutzern einen umfassenden รœberblick รผber die wichtigsten Aspekte der IT-Sicherheit in einer sich stรคndig verรคndernden digitalen Welt.

16.12.2023 - TIP: Wer den Cookie Consent Banner akzeptiert, kann z.B. von Englisch nach Deutsch รผbersetzen, erst Englisch auswรคhlen dann wieder Deutsch!

Google Android Playstore Download Button fรผr Team IT Security



๐Ÿ“š Recreating the Apple Calculator in Rust using Tauri, Yew and Tailwind


๐Ÿ’ก Newskategorie: Programmierung
๐Ÿ”— Quelle: dev.to

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.

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

And if you want to see more of my work, feel free to explore these links: Twitter YouTube

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.

To create a Tailwind configuration file, run:

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:

/** @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:

@tailwind base;
@tailwind components;
@tailwind utilities;

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

<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:

{
.  ...
  "tauri": {
    ...

    "windows": [
      {
        "titleBarStyle": "Transparent",
        "title": ""
      }
    ]
  }
}

Setting up the calculator

Now, let's set the foundation for our calculator's logic. We begin by defining essential states that will drive its functionality:

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:

#[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:

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

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:

#[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:

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:

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:

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:

pub mod action_button;
pub mod button_styles;
pub mod number_button;

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

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:

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:

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:

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:

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

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:

  <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:

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

Final row

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

<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:

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

I hope you found this tutorial insightful and engaging. For more content and updates, connect with me on Twitter and YouTube.

...



๐Ÿ“Œ Recreating the Apple Calculator in Rust using Tauri, Yew and Tailwind


๐Ÿ“ˆ 132.84 Punkte

๐Ÿ“Œ Create a Full stack Rust desktop App with Tauri, Yew and Tailwind CSS


๐Ÿ“ˆ 82.3 Punkte

๐Ÿ“Œ Let's Build a RUST WebAssembly Frontend App With Yew


๐Ÿ“ˆ 40.1 Punkte

๐Ÿ“Œ How To Create a Calculator Using HTML CSS & JavaScript | Simple Calculator in JavaScript


๐Ÿ“ˆ 36.1 Punkte

๐Ÿ“Œ Recreating YouTube Using HTML and CSS


๐Ÿ“ˆ 34.8 Punkte

๐Ÿ“Œ Tauri: Schlanker Electron-Herausforderer in Rust


๐Ÿ“ˆ 34.51 Punkte

๐Ÿ“Œ Tauri: Schlanker Electron-Herausforderer in Rust


๐Ÿ“ˆ 34.51 Punkte

๐Ÿ“Œ Build Your Own Currency Calculator App with Next.js 13, Node.js, Firebase, TypeScript and Tailwind CSS


๐Ÿ“ˆ 32.68 Punkte

๐Ÿ“Œ Build a Profit Margin Calculator with Vite.js + React.js, TypeScript and Tailwind CSS


๐Ÿ“ˆ 32.68 Punkte

๐Ÿ“Œ Rebuild an EMI Calculator without Next.js, TypeScript, Tailwind CSS, Recoil and Recharts


๐Ÿ“ˆ 32.68 Punkte

๐Ÿ“Œ Create reusable button Components with React,Typescript , Tailwind and Tailwind-variants


๐Ÿ“ˆ 32.63 Punkte

๐Ÿ“Œ Streamline Your Security Position and Strengthen It in the Process - Richard Yew - RSA23 #2


๐Ÿ“ˆ 32.38 Punkte

๐Ÿ“Œ Microsoft Calculator (UWP). Telemetry in a freaking calculator....


๐Ÿ“ˆ 30.93 Punkte

๐Ÿ“Œ Is the best calculator on Android a port of the Windows Calculator?


๐Ÿ“ˆ 30.93 Punkte

๐Ÿ“Œ Coffee with the Council Podcast: A Panel Discussion from Asia-Pacific Hosted by Yew Kuann Cheng


๐Ÿ“ˆ 30.58 Punkte

๐Ÿ“Œ Deobfuscating Emotet's JavaScript dropper and recreating the original code step-by-step


๐Ÿ“ˆ 29.63 Punkte

๐Ÿ“Œ Characterizing a workload and recreating it as a synthetic benchmark


๐Ÿ“ˆ 29.63 Punkte

๐Ÿ“Œ Recreating Steam - Medusa & Next.js


๐Ÿ“ˆ 27.82 Punkte

๐Ÿ“Œ Recreating Andrej Karpathyโ€™s Weekend Projectโ€Šโ€”โ€Ša Movie Search Engine


๐Ÿ“ˆ 27.82 Punkte

๐Ÿ“Œ Valorant Player Is Recreating The Entire โ€˜Practice Rangeโ€™ In Minecraft


๐Ÿ“ˆ 27.82 Punkte

๐Ÿ“Œ While Recreating CentOS as 'Rocky Linux', Gregory Kurtzer Also Launches a Sponsoring Startup


๐Ÿ“ˆ 27.82 Punkte

๐Ÿ“Œ Progress Continues On Recreating the Babbage Programmable Computer


๐Ÿ“ˆ 27.82 Punkte

๐Ÿ“Œ CrabNebula and Tauri: Pioneering Resilient App Development Together


๐Ÿ“ˆ 26.79 Punkte

๐Ÿ“Œ Rust Basics Series #2: Using Variables and Constants in Rust Programs


๐Ÿ“ˆ 26.02 Punkte

๐Ÿ“Œ CVE-2022-39215 | Tauri up to 1.0.5 Symbolic readDir path traversal (GHSA-28m8-9j7v-x499)


๐Ÿ“ˆ 24.99 Punkte

๐Ÿ“Œ CVE-2022-41874 | Tauri up to 1.0.6/1.1.1 Filesystem Scope access control (GHSA-q9wv-22m9-vhqh)


๐Ÿ“ˆ 24.99 Punkte

๐Ÿ“Œ This Week In React #128: SWR, Vite, Codux, Storybook, Next.js, Forget, Nylon, Paper, align-deps, INP, Zod, Tauri...


๐Ÿ“ˆ 24.99 Punkte

๐Ÿ“Œ Tauri - The Flutter killer?


๐Ÿ“ˆ 24.99 Punkte

๐Ÿ“Œ Red Bull wird Alpha Tauri laut Marko nicht verkaufen


๐Ÿ“ˆ 24.99 Punkte

๐Ÿ“Œ Moving from Electron to Tauri 2


๐Ÿ“ˆ 24.99 Punkte

๐Ÿ“Œ CVE-2023-46115 | Tauri 1.5.5 vite.config.ts TAURI_PRIVATE_KEY/TAURI_KEY_PASSWORD insufficiently protected credentials (GHSA-2rcp-jvr4-r259)


๐Ÿ“ˆ 24.99 Punkte

๐Ÿ“Œ Goodbye Electron. Hello Tauri!


๐Ÿ“ˆ 24.99 Punkte

๐Ÿ“Œ Electron vs Tauri for your next project


๐Ÿ“ˆ 24.99 Punkte

๐Ÿ“Œ Tauri: Fast, Cross-platform Desktop Apps


๐Ÿ“ˆ 24.99 Punkte

๐Ÿ“Œ Announcing DevTools for Tauri


๐Ÿ“ˆ 24.99 Punkte











matomo