I have a Rust Tauri GUI app with two elements: A play-button and a text field displaying an amount of seconds passed in a timer. The goal is simple: Clicking the button should start a timer, clicking the button again should pause it. This should be toggled.
The code I currently have kind of works but feels really buggy. Clicking the button starts the timer. But when I click to pause it, it takes sometimes like 10 seconds before the app detects the pause-click. Sometimes the app also freezes.
I think this has something to do with my thread- or arc/mutex/lock-handling.
Rust code:
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::sync::{mpsc, Arc, Mutex, Condvar};
use tauri::{Manager, State};
struct IsPlayingState(Arc<(Mutex<bool>, Condvar)>);
struct FirstClickState(Arc<Mutex<bool>>);
fn counter(tx: mpsc::Sender<u32>, is_playing: Arc<(Mutex<bool>, Condvar)>) {
let mut seconds: u32 = 0;
loop {
let (lock, cvar) = &*is_playing;
let mut playing = lock.lock().unwrap();
println!("Playing {:?}", playing);
while !*playing {
playing = cvar.wait(playing).unwrap();
}
if *playing {
println!("Time {}", seconds);
tx.send(seconds).expect("Failed to transmit seconds");
seconds += 1;
std::thread::sleep(std::time::Duration::from_secs(1));
}
}
}
#[tauri::command]
fn play(app_handle: tauri::AppHandle, is_playing: State<IsPlayingState>, first_click: State<FirstClickState>) {
let (tx, rx) = mpsc::channel();
let is_playing_clone = Arc::clone(&is_playing.0);
let mut first_click_lock = first_click.0.lock().unwrap();
let (lock, cvar) = &*is_playing.0;
let mut playing = lock.lock().unwrap();
*playing = !*playing;
println!("Playing 2 {:?}", playing);
if *playing {
cvar.notify_one();
}
if *first_click_lock {
println!("rofl");
std::thread::spawn(move || {
counter(tx, is_playing_clone);
});
std::thread::spawn(move || {
while let Ok(seconds) = rx.recv() {
app_handle.emit_all("update_time", seconds).expect("Failed to emit seconds");
}
});
*first_click_lock = false;
}
}
fn main() {
let is_playing = Arc::new((Mutex::new(false), Condvar::new()));
let first_click = Arc::new(Mutex::new(true));
tauri::Builder::default()
.manage(IsPlayingState(is_playing))
.manage(FirstClickState(first_click))
.invoke_handler(tauri::generate_handler![play])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Javascript (Tauri):
const { invoke } = window.__TAURI__.tauri;
const { listen } = window.__TAURI__.event;
async function play() {
await invoke("play");
}
window.addEventListener("DOMContentLoaded", () => {
document.getElementById("play-button").addEventListener("click", (e) => {
e.preventDefault();
play();
});
listen("update_time", (event) => {
const seconds = event.payload;
const time = new Date(seconds * 1000).toISOString().slice(11, 19);
document.getElementById("time").textContent = `${time}`;
});
});
HTML (Tauri):
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="styles.css" />
<link rel="stylesheet" href="app.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Time Logger</title>
<script type="module" src="/main.js" defer></script>
</head>
<body>
<div class="container">
<img id="play-button" alt=">" title="Start time" src="assets/play-button.png">
<span id="time">00:00:00</span>
</div>
</body>
</html>
In this video you can see the problem. Clicking to play the timer works fine, but when clicking it to pause the timer keeps running for a few seconds before actually pausing the counter.
Edit: When I change the thread to sleep 100ms instead of 1s. And calculate the amount of time passed based on ms. The code runs a lot better. I think because the threads sleeps for 1s it is sometimes blocked from reading the playing
variable state.