Kalculator: It's like a calculator but with a 'k' instead of a 'c'

Following development

I'm working on this feature on my twitch stream, if you're interested in the development process. Which means this is a live document, which means the contents will be updated as I get deeper into the development of the program.

I've always wanted to make a GUI program in Rust, and I keep bouncing between crates, and I thought "No, I am not going to bounce, I am going to stick with it." I heard that egui is a decent enough GUI crate for beginners in Rust so I took a shot at it.

It started off rocky, had a lot of issues that I eventually manged to figure out a few issues with the help of a viewer on my twitch stream.

Designing the GUI

The gui was very simple, creating buttons wasn't hard at all:

fn button(ui: &mut egui::Ui, text: &str) -> egui::Response {
    let button = egui::Button::new(text);
    let button = ui.add_sized(egui::Vec2 { x: 40.0, y: 40.0 }, button);
    button
}

Once I figured out the code to create a button with different font sizes, adjusting the padding around the buttons it wasn't that hard to just replicate it about 10 times for the operators, plus, minus, division, etc.

Turns out I just had to dereference the mutable string. I was doing this the whole time to update the label

// display is of type &mut std::string::String
let Self { display } = self;
display = format!("{}{}", display, "1".to_string());

There were a bunch of errors saying that it was moved into the scope of the closure, and the issue was very simple. All I had to do was to add a dereference operator.

*display = format!("{}{}", display, "1".to_string());

All it took was an asterisk so solve all my issues, I was scratching at my head for hours. So this is a lesson, that I have learnt (hopefully).

Performing the calculations

Now this was the actual hard part, because I needed to creating something called an AST, an abstract syntax tree. The GUI was just sugar. Now to the actual grunt work, I had to look through 3 resources that was suggested to me by a discord user. They are createlang, how to build a calculator, and Abstract syntax trees.

After "some" consideration I think the resource named "How to build a calculator" should be the most effective one in teaching me how to build a calculator. Basically, I need to create a lexer that breaks down a string by detecting the valid tokens. It's worth nothing that this isn't the actual definition, because I'm basing my definition based on the rust code that I wrote:

pub fn tokenize(self) -> Vec<Token> {
        let mut tokens: Vec<Token> = vec![];

        let x = self.input.chars().map(|ch| {
            match ch {
                '+' => return tokens.push(Token::new(TokenType::Plus, '+')),
                '-' => return tokens.push(Token::new(TokenType::Minus, '-')),
                '/' => return tokens.push(Token::new(TokenType::Divide, '/')),
                '*' => return tokens.push(Token::new(TokenType::Multiply, '*')),
                ' ' => {},
                _ => {
                    if !ch.is_numeric() {
                        return tokens.push(Token::new(TokenType::Eof, 'e'))
                    } else {
                        return tokens.push(Token::new(TokenType::Int, ch))
                    }
                }
            };
        });
        tokens
    }

What it does is that it takes all the characters in the string and matches it to the characters. If it's a match it adds the appropriate token to the vector. Then returns it. This on it's own took a while to implement, as I've actually never used iterators this way. Usually I'd use a for loop and implement the logic on my own, but I've read that the Rust std is really powerful.

After getting help from a streamer, when you use map on an iterable, the return result is of type Map, so I need to evaluate it, which means I just need to run a function on the Map. Because nothing inside the closure is actually executed until it is called upon from the Map. And I've decided to use a standard for loop instead.

    pub fn tokenize(self) -> Vec<Token> {
        let mut tokens: Vec<Token> = vec![];

        // Before it was with a map(), but it needs to be used
        // This is just easier with a for loop.
        for ch in self.input.chars() {
            match ch {
                '+' => tokens.push(Token::new(TokenType::Plus, '+')),
                '-' => tokens.push(Token::new(TokenType::Minus, '-')),
                '/' => tokens.push(Token::new(TokenType::Divide, '/')),
                '*' => tokens.push(Token::new(TokenType::Multiply, '*')),
                '%' => tokens.push(Token::new(TokenType::Percent, '%')),
                ' ' => {},
                _ => {
                    if !ch.is_numeric() {
                        tokens.push(Token::new(TokenType::Eof, 'e'))
                    } else {
                        tokens.push(Token::new(TokenType::Int, ch))
                    }
                }
            };
        }
        tokens
    }

I also added the Percent variant to TokenType as a valid token.

Implementing a binary tree

The binary tree will be the AST, because each expression can be separated into their own node, for example take this as an example: 4 * 2 the answer is 8.

We know that we have to do multiplication then addition. The tokenizer will be able to break apart that expression into this set of outputs:

Token { token_type: Int, left: '4', right: "*" }
Token { token_type: Multiply, left: '*', right: "2" }