use std::sync::{Arc, Mutex};
use corundum_framework_package::{After, Updater};
use corundum_framework_update::PackageState;
use crate::{icons, tr};
pub struct ReadyBadge {
updater: Updater,
error: Arc<Mutex<Option<String>>>,
applying: Arc<Mutex<bool>>,
#[cfg(test)]
shown: bool,
}
impl ReadyBadge {
pub fn new(updater: Updater) -> ReadyBadge {
ReadyBadge {
updater,
error: Arc::new(Mutex::new(None)),
applying: Arc::new(Mutex::new(false)),
#[cfg(test)]
shown: false,
}
}
pub fn ui(&mut self, ui: &mut egui::Ui) {
let state = self.updater.status().state;
let ready = match &state {
PackageState::Ready { version } => Some(version.to_string()),
_ => None,
};
let mut error = self.error.lock().unwrap_or_else(|e| e.into_inner());
if ready.is_none()
&& !matches!(state, PackageState::Failed(_))
&& !*self.applying.lock().unwrap_or_else(|e| e.into_inner())
{
*error = None;
}
let error = error.clone();
let shown = ready.is_some() || error.is_some();
#[cfg(test)]
{
self.shown = shown;
}
if !shown {
return;
}
let failed = error.is_some();
let hover = error.unwrap_or_else(|| {
tr!(
ui,
"corundum-update-badge",
version = ready.unwrap_or_default()
)
});
let response = ui
.scope(|ui| {
if failed {
ui.visuals_mut().override_text_color = Some(ui.visuals().warn_fg_color);
}
ui.add(icons::button(icons::download(), 14.0))
})
.inner
.on_hover_text(hover);
if response.clicked() {
self.restart(ui.ctx());
}
}
fn restart(&self, ctx: &egui::Context) {
{
let mut applying = self.applying.lock().unwrap_or_else(|e| e.into_inner());
if *applying {
return;
}
*applying = true;
}
*self.error.lock().unwrap_or_else(|e| e.into_inner()) = None;
let (updater, error, applying, ctx) = (
self.updater.clone(),
Arc::clone(&self.error),
Arc::clone(&self.applying),
ctx.clone(),
);
std::thread::spawn(move || {
let _reset = ResetOnDrop(applying, ctx.clone());
match updater.apply(After::RestartApp) {
Ok(()) => {
ctx.send_viewport_cmd_to(egui::ViewportId::ROOT, egui::ViewportCommand::Close)
}
Err(err) => {
*error.lock().unwrap_or_else(|e| e.into_inner()) = Some(err.to_string());
}
}
});
}
}
struct ResetOnDrop(Arc<Mutex<bool>>, egui::Context);
impl Drop for ResetOnDrop {
fn drop(&mut self) {
*self.0.lock().unwrap_or_else(|e| e.into_inner()) = false;
self.1.request_repaint();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::render;
fn shown(state: PackageState) -> bool {
let mut badge = ReadyBadge::new(Updater::for_tests(state));
render([600.0, 400.0], |ui| badge.ui(ui));
badge.shown
}
#[test]
fn an_up_to_date_package_draws_no_badge() {
assert!(!shown(PackageState::UpToDate));
}
#[test]
fn a_downloading_package_draws_no_badge() {
assert!(!shown(PackageState::Downloading { percent: 40 }));
}
#[test]
fn a_ready_update_draws_the_badge() {
assert!(shown(PackageState::Ready {
version: "1.1.0".parse().unwrap()
}));
}
fn clicked(updater: Updater) -> (ReadyBadge, egui::Context) {
let badge = ReadyBadge::new(updater);
let ctx = egui::Context::default();
badge.restart(&ctx);
while *badge.applying.lock().unwrap() {
std::thread::yield_now();
}
(badge, ctx)
}
fn clicked_failing() -> ReadyBadge {
clicked(Updater::for_tests(PackageState::Failed("stop".into()))).0
}
fn closes(ctx: &egui::Context) -> bool {
let mut output = ctx.run_ui(egui::RawInput::default(), |_| {});
output.textures_delta.clear();
output.viewport_output.values().any(|v| {
v.commands
.iter()
.any(|c| matches!(c, egui::ViewportCommand::Close))
})
}
#[test]
fn a_restart_handed_to_velopack_closes_the_window() {
let (badge, ctx) = clicked(Updater::for_tests(PackageState::Ready {
version: "1.1.0".parse().unwrap(),
}));
assert!(closes(&ctx));
assert!(badge.error.lock().unwrap().is_none());
}
#[test]
fn a_failed_restart_keeps_the_window() {
let (_badge, ctx) = clicked(Updater::for_tests(PackageState::Failed("stop".into())));
assert!(!closes(&ctx));
}
#[test]
fn a_failed_restart_keeps_the_badge_with_its_error() {
let mut badge = clicked_failing();
render([600.0, 400.0], |ui| badge.ui(ui));
assert!(badge.shown);
let error = badge.error.lock().unwrap().clone();
assert!(error.is_some_and(|e| e.contains("nothing downloaded")));
}
#[test]
fn a_new_check_ends_the_failure_the_badge_showed() {
let badge = clicked_failing();
let mut ready = ReadyBadge::new(Updater::for_tests(PackageState::Checking));
ready.error = Arc::clone(&badge.error);
render([600.0, 400.0], |ui| ready.ui(ui));
assert!(!ready.shown);
assert!(badge.error.lock().unwrap().is_none());
}
#[test]
fn a_ready_badge_is_an_enabled_button() {
let mut badge = ReadyBadge::new(Updater::for_tests(PackageState::Ready {
version: "1.1.0".parse().unwrap(),
}));
let mut enabled = false;
render([600.0, 400.0], |ui| {
badge.ui(ui);
enabled = ui.is_enabled() && badge.shown;
});
assert!(enabled);
}
}