okgr

Introduction to Rust πŸ¦€ with egui

State and Logic

In this section, we have two objectives. We’re going to implement:

  • the state for our app. But we need questions. I mean you should get some questions.
  • and wire the logic with the UI

State

Our state is a Quiz. Luckily, we’ve already implemented that. So let’s add that to our App struct.

struct App {
  quiz: quiz::Quiz
}

You’ll get an error: struct 'Quiz' is private. What we need to do is add pub to the Quiz struct in quiz.rs:

pub struct Quiz {
  // ...
}

And we’ll have to update our App::new function:

impl App {
  fn new() -> Self {
    let quiz = quiz::Quiz::sample();
    Self { quiz }
  }
}

::sample() is not implemented for quiz::Quiz. Want to give it a try?

The following is an implementation of the sample function:

impl Quiz {
  pub fn sample() -> Self {
    Self {
      // Notice how we create a vector! This is a macro that
      // helps us create a vector with less code. Read on macros πŸ€·πŸ½β€β™‚οΈ
      questions: vec![
        Question {
          title: "Is the sky blue?".to_string(),
          answer: true,
          user_answer: None
        },
        Question {
          title: "Is the grass green?".to_string(),
          answer: true,
          user_answer: None
        },
        Question {
          title: "Is the sun yellow?".to_string(),
          answer: false,
          user_answer: None
        },
      ],
      current_index: 0
    }
  }

  // ...
}

::sample() here doesn’t accept a &self unlike the other functions we’ve seen. You can treat functions like this as static functions. In Typescript, this would look like:

class Quiz {
  static sample() {
    return new Quiz(/* ... */)
  }
}

This same format (ie, static functions) is used for constructors. In Rust, constructors are just static functions that return a new instance of the struct. Conventionally, you can call your constructor new.

impl Quiz {
  fn new(questions: Vec<Question>) -> Self {
    Self {
      questions
    }
  }
}

Logic

Back to our App::update function, let’s actually render the current question and other relevant information:

impl eframe::app::App for App {
  fn update(&mut self, ctx: &egui::CtxRef, frame: &mut eframe::epi::Frame<'_>) {
    egui::CentralPanel::default().show(ctx, |ui| {
      let current_index = self.quiz.current_index + 1;
      let count = self.quiz.questions.len();

      // welcome to string formatting in Rust
      ui.label(format!("{current_index}/{count}"));

      let current_question = self.quiz.current_question();
      ui.label(&current_question.title);

      // ...
    });
  }
}

Adding this piece of code will introduce a number of errors in the format:

field `current_index` of `Quiz` is private: rust-analyzer (E0616)

Add pub to these fields, methods or structs to make them accessible to the main module.

struct Quiz {
  current_index: usize;
  pub current_index: usize;
  // ...
}

Let’s move on to add the buttons:

impl eframe::app::App for App {
  fn update(&mut self, ctx: &egui::CtxRef, frame: &mut eframe::epi::Frame<'_>) {
    egui::CentralPanel::default().show(ctx, |ui| {
      // ...

      ui.horizontal(|ui| {
        // Conventionally, you would have attached a click handler
        // to the button. But remember, this is an immediated mode UI.
        // So we have to perform this check every frame.
        if ui.button("True").clicked() {
          self.quiz.answer(true);
        }
        if ui.button("False").clicked() {
          self.quiz.answer(false);
        }
      });

      // ...
    });
  }
}

Next, Previous?

Here’s your exercise:

  1. Implement the next and previous buttons. The question number and text should update accordingly.
  2. On the same row as True/False buttons, show a label only if the user has answered the question. The label should say whether the user got the question right or wrong. β€œCorrect” or β€œWrong” is fine.

⚠️ Remember that your next button will only work when you answer the current question. So click True/False first.

Let’s rearrange our UI next.