First application in Rust + Slint

Hey all, I'm relatively new to Rust, and this is my first toy application using Rust and Slint. I did this to just start getting a hang of both. I'd appreciate feedback!

The application is a very simple calculator, with basic math ops.

use std::cell::RefCell;
use std::rc::{Rc, Weak};

slint::slint! {
import { Button, LineEdit } from "std-widgets.slint";

export enum CalculatorViewOp { Add, Subtract, Multiply, Divide, Evaluate }

export component CalculatorView {
        callback input(float);
        callback operation(CalculatorViewOp);

        in property <float> current_value: 0;

        VerticalLayout {
            alignment: stretch;
            padding: 15px;
            spacing: 5px;

            Text {
                text: "Awesome Calculator";
                horizontal-alignment: center;
            }
            LineEdit {
                vertical-stretch: 0;
                horizontal-alignment: right;
                input-type: decimal;
                text: current_value;
            }
            GridLayout {
                vertical-stretch: 1;
                spacing: 5px;

                Row {
                    Button {
                        text: "1";
                        clicked => { root.input(1); }
                    }
                    Button {
                        text: "2";
                        clicked => { root.input(2); }
                    }
                    Button {
                        text: "3";
                        clicked => { root.input(3); }
                    }
                }
                Row {
                    Button {
                        text: "4";
                        clicked => { root.input(4); }
                    }
                    Button {
                        text: "5";
                        clicked => { root.input(5); }
                    }
                    Button {
                        text: "6";
                        clicked => { root.input(6); }
                    }
                }
                Row {
                    Button {
                        text: "7";
                        clicked => { root.input(7); }
                    }
                    Button {
                        text: "8";
                        clicked => { root.input(8); }
                    }
                    Button {
                        text: "9";
                        clicked => { root.input(9); }
                    }
                }
                Row {
                    Button {
                        text: "0";
                        clicked => { root.input(0); }
                    }
                }
                Button {
                    text: "+";
                    col: 3;
                    row: 0;
                    clicked => { root.operation(CalculatorViewOp.Add); }
                }
                Button {
                    text: "-";
                    col: 3;
                    row: 1;
                    clicked => { root.operation(CalculatorViewOp.Subtract); }
                }
                Button {
                    text: "×";
                    col: 3;
                    row: 2;
                    clicked => { root.operation(CalculatorViewOp.Multiply); }
                }
                Button {
                    text: "÷";
                    col: 3;
                    row: 3;
                    clicked => { root.operation(CalculatorViewOp.Divide); }
                }
                Button {
                    text: "=";
                    row: 3;
                    colspan: 2;
                    col: 1;
                    clicked => { root.operation(CalculatorViewOp.Evaluate); }
                }
            }
        }
    }
}

#[derive(Copy, Clone)]
enum CalculatorMathOps {
    Add,
    Subtract,
    Multiply,
    Divide,
}

#[derive(Copy, Clone)]
enum CalculatorAction {
    Number(f32),
    Op(CalculatorMathOps),
    Evaluate,
}

#[derive(Default)]
struct CalculatorViewModel {
    memory: Option<f32>,
    user_entered_value: Option<f32>,
    op_value: Option<f32>,
    operation: Option<CalculatorMathOps>,
}

impl CalculatorViewModel {
    fn perform_calculation(&mut self) {
        let prev_op = self.operation.unwrap();

        let memory = self.memory.unwrap();
        let value = if self.user_entered_value.is_some() {
            self.user_entered_value.unwrap()
        } else {
            self.op_value.unwrap()
        };

        let computed_value = match prev_op {
            CalculatorMathOps::Add => memory + value,
            CalculatorMathOps::Subtract => memory - value,
            CalculatorMathOps::Multiply => memory * value,
            CalculatorMathOps::Divide => memory / value,
        };

        self.memory = Some(computed_value);
    }

    fn can_perform_calculation(&self, use_op_value: bool) -> bool {
        self.memory.is_some()
            && (self.user_entered_value.is_some() || (use_op_value && self.op_value.is_some()))
    }

    fn perform_action(&mut self, action: CalculatorAction) {
        match action {
            CalculatorAction::Number(n) => {
                self.user_entered_value = Some((self.user_entered_value.unwrap_or_default() * 10.0) + n);
                self.op_value = self.user_entered_value;
            }
            CalculatorAction::Evaluate if self.can_perform_calculation(true) => {
                self.perform_calculation();
                self.user_entered_value = None;
            }
            CalculatorAction::Op(op) if self.can_perform_calculation(false) => {
                self.perform_calculation();
                self.operation = Some(op);
            }
            CalculatorAction::Op(op) if self.user_entered_value.is_some() => {
                self.memory = self.user_entered_value;
                self.op_value = None;
                self.user_entered_value = None;
                self.operation = Some(op);
            }
            CalculatorAction::Op(op) => {
                self.op_value = None;
                self.user_entered_value = None;
                self.operation = Some(op);
            }
            _ => {}
        }
    }
}

fn invalidate_view(
    vm_weak: Weak<RefCell<CalculatorViewModel>>,
    view_weak: slint::Weak<CalculatorView>,
) {
    let view = view_weak.upgrade().unwrap();
    let vm_strong = vm_weak.upgrade().unwrap();
    let vm = vm_strong.borrow();

    if let Some(value) = vm.user_entered_value {
        view.set_current_value(value);
    } else if let Some(value) = vm.memory {
        view.set_current_value(value);
    }
}

fn main() {
    let vm: Rc<RefCell<CalculatorViewModel>> =
        Rc::new(RefCell::new(CalculatorViewModel::default()));
    let vm_weak = Rc::downgrade(&vm);

    let view = CalculatorView::new().unwrap();
    let view_weak = view.as_weak();

    view.on_input(move |id| {
        {
            let vm_strong = vm_weak.upgrade().unwrap();
            let mut vm = vm_strong.borrow_mut();
            vm.perform_action(CalculatorAction::Number(id));
        }

        invalidate_view(vm_weak.clone(), view_weak.clone());
    });

    let vm_weak = Rc::downgrade(&vm);
    let view_weak = view.as_weak();

    view.on_operation(move |op| {
        let vm_strong = vm_weak.upgrade().unwrap();

        {
            let mut vm = vm_strong.borrow_mut();

            match op {
                CalculatorViewOp::Add => {
                    vm.perform_action(CalculatorAction::Op(CalculatorMathOps::Add));
                }
                CalculatorViewOp::Subtract => {
                    vm.perform_action(CalculatorAction::Op(CalculatorMathOps::Subtract));
                }
                CalculatorViewOp::Multiply => {
                    vm.perform_action(CalculatorAction::Op(CalculatorMathOps::Multiply));
                }
                CalculatorViewOp::Divide => {
                    vm.perform_action(CalculatorAction::Op(CalculatorMathOps::Divide));
                }
                CalculatorViewOp::Evaluate => {
                    vm.perform_action(CalculatorAction::Evaluate);
                }
            }
        }

        invalidate_view(vm_weak.clone(), view_weak.clone());
    });

    view.run().unwrap();
}

I can't speak to the Slint view as I'm not familiar enough with that framework.

But I will talk about your rust code.

let value = if self.user_entered_value.is_some() {
    self.user_entered_value.unwrap()
} else {
    self.op_value.unwrap()
};

This works, but would be better expressed with Option::or:

let value = self.user_entered_value
    .or(self.op_value)
    .unwrap();

I'm a little concerned by the amount of unwrap(), particularly as they rely on assumptions about internal state before the function is called.
This might work now but is prone to accidental breakage when making changes later - you could consider using expect() or returning early if things are unexpectedly None:

let Some(value) = self.user_entered_value else {
    return
};
// use value from here on

Having said that, you've put the pre-checks in a method that you call when matching the action which I think helps.

I like the use of enums for different actions and the Ops.

There's a lot of Rc<RefCell<...>> going on, which I'll chalk down to "just Slint things". Good job on figuring it all out, it looks quite complicated to me.

Oh and of course, run cargo clippy if you haven't yet. The lints are well worth adhering to, I am always learning things and improving my code thanks to clippy lints :slight_smile:

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.