QKQL
QKQL

Reputation: 35

What is the best way to update app fields from another thread in egui in rust

I'm trying to run a function in another thread that updates app fields every second and window accordingly. But I don't know how to do that. What is the best way to do that? I tried to use Arc.

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release

use std::{
    sync::{Arc, Mutex},
    thread,
    time::Duration,
};

use eframe::egui::{self};

fn main() {
    let options = eframe::NativeOptions::default();
    let app = MyApp::default();
    let arc = Arc::new(Mutex::new(app));
    thread::spawn(|| {
        thread::sleep(Duration::from_secs(1));
        arc.lock().unwrap().field += 1;
    });
    eframe::run_native(
        "Test",
        options,
        Box::new(move |_cc| Box::new(arc.lock().unwrap())),
    );
}

struct MyApp {
    field: i128,
}

impl Default for MyApp {
    fn default() -> Self {
        Self { field: 0 }
    }
}

impl eframe::App for MyApp {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        egui::CentralPanel::default().show(ctx, |ui| ui.label(format!("{}", self.field)));
    }
}

But I got this error:

error[E0277]: the trait bound `MutexGuard<'_, MyApp>: App` is not satisfied
  --> src/main.rs:22:29
   |
22 |         Box::new(move |_cc| Box::new(arc.lock().unwrap())),
   |                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `App` is not implemented for `MutexGuard<'_, MyApp>`
   |
   = note: required for the cast to the object type `dyn App`

For more information about this error, try `rustc --explain E0277`.

I think that I do it the wrong way.

Upvotes: 1

Views: 4158

Answers (2)

Mikael Nilsson
Mikael Nilsson

Reputation: 51

I'm new to Rust and egui/eframe as well. I wanted to build on the answer from BeaconBrigade and add an automatic repaint request from the thread. On Youtube there are two videos Andrei Litvin's https://www.youtube.com/watch?v=zUvHkkkrmIY and CreativeCoder's https://www.youtube.com/watch?v=NtUkr_z7l84. They both force the gui to update at 60fps. My code example illustrates how to only repaint the GUI on demand (from the thread) or on mouse hover etc., otherwise the egui update function will be idle and not run.

use eframe::egui;
use rand::Rng;
use std::sync::{Arc, Mutex};

fn slow_process(state_clone: Arc<Mutex<State>>) {
    loop {
        let duration = rand::thread_rng().gen_range(1000..3000);
        println!("going to sleep for {}ms", duration);
        std::thread::sleep(std::time::Duration::from_millis(duration));
        state_clone.lock().unwrap().duration = duration;
        let ctx = &state_clone.lock().unwrap().ctx;
        match ctx {
            Some(x) => x.request_repaint(),
            None => panic!("error in Option<>"),
        }
    }
}

struct State {
    duration: u64,
    ctx: Option<egui::Context>,
}

impl State {
    pub fn new() -> Self {
        Self {
            duration: 0,
            ctx: None,
        }
    }
}

pub struct App {
    state: Arc<Mutex<State>>, 
}

impl App {
    pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
        let state = Arc::new(Mutex::new(State::new()));
        state.lock().unwrap().ctx = Some(cc.egui_ctx.clone());
        let state_clone = state.clone();
        std::thread::spawn(move || {
            slow_process(state_clone);
        });
        Self {
            state,
        }
    }
}

impl eframe::App for App {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        egui::CentralPanel::default().show(ctx, |ui| {
            ui.label(format!("woke up after {}ms", self.state.lock().unwrap().duration));
        });
        println!(".");
    }
}

fn main() {
    let native_options = eframe::NativeOptions::default();
    eframe::run_native(
        "eframe template",
        native_options,
        Box::new(|cc| Box::new(App::new(cc))),
    );
}

Upvotes: 5

BeaconBrigade
BeaconBrigade

Reputation: 56

I'm new to egui, but I think you're not able to wrap MyApp in an Arc<Mutex<T>> because the closure in Box::new(move |_cc| Box::new(arc.lock().unwrap())), expects your App in a Box<T> but when you lock the Mutex, it will give you a MutexGuard. Even if this worked, since run_native takes your locked mutex, no one else will be able to unlock it so no thread can modify that data.

To fix this, you could either update the state in update or wrap your data (not app) in an Arc<Mutex<T>>. Below is one way to fix it. You actually have to move your mouse or click or hit a key to update the view.

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release

use std::{
    sync::{Arc, Mutex},
    thread,
    time::Duration,
};

use eframe::egui;

fn main() {
    let options = eframe::NativeOptions::default();
    let app = MyApp::default();
    let field = app.field.clone();
    thread::spawn(move || {
        loop {
            thread::sleep(Duration::from_secs(1));
            *field.lock().unwrap() += 1;
        }
    });
    eframe::run_native(
        "Test",
        options,
        Box::new(move |_cc| Box::new(app)),
    );
}
 
struct MyApp {
    field: Arc<Mutex<i128>>,
}
 
impl Default for MyApp {
    fn default() -> Self {
        Self { field: Arc::new(Mutex::new(0)) }
    }
}
 
impl eframe::App for MyApp {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        egui::CentralPanel::default().show(ctx, |ui| ui.label(format!("{}", self.field.lock().unwrap())));
    }
}

Upvotes: 4

Related Questions