use eframe::egui::{self, Color32, RichText};
use std::sync::mpsc::{self, Receiver, Sender};
use std::time::Duration;
use crate::detect::{self, Stage};
const STAGES: [(Stage, &str); 4] = [
(Stage::Enum, "硬件枚举"),
(Stage::Driver, "驱动检查"),
(Stage::Func, "功能自检"),
(Stage::Sample, "稳定性采样"),
];
#[derive(Clone, Debug)]
pub struct GuiOptions {
pub samples: u32,
pub auto: bool,
pub close_secs: u32,
}
pub struct GuiResult {
pub status: bool,
pub content: String,
}
#[derive(Clone, Debug)]
enum StepState {
Waiting,
Running,
Ok(String),
Fail(String),
Skipped,
}
enum Msg {
Progress(Stage, Option<bool>, String),
Done(bool, String),
}
struct App {
opts: GuiOptions,
steps: Vec<(Stage, &'static str, StepState)>,
detail: String,
result: Option<bool>,
countdown: Option<u32>,
rx: Receiver<Msg>,
last_tick: std::time::Instant,
result_slot: std::sync::Arc<std::sync::Mutex<Option<GuiResult>>>,
}
impl App {
fn new(opts: GuiOptions, rx: Receiver<Msg>, result_slot: std::sync::Arc<std::sync::Mutex<Option<GuiResult>>>) -> Self {
let steps = STAGES.iter().map(|&(s, label)| (s, label, StepState::Waiting)).collect();
Self {
opts,
steps,
detail: String::new(),
result: None,
countdown: None,
rx,
last_tick: std::time::Instant::now(),
result_slot,
}
}
fn handle_progress(&mut self, stage: Stage, ok: Option<bool>, detail: String) {
if let Some(slot) = self.steps.iter_mut().find(|(s, _, _)| *s == stage) {
slot.2 = match ok {
None => StepState::Running,
Some(true) => StepState::Ok(detail),
Some(false) => StepState::Fail(detail),
};
}
}
fn finish(&mut self, status: bool, content: String) {
self.result = Some(status);
self.detail = content;
for (_, _, state) in &mut self.steps {
if matches!(state, StepState::Waiting) {
*state = StepState::Skipped;
}
}
if self.opts.auto {
self.countdown = Some(self.opts.close_secs);
self.last_tick = std::time::Instant::now();
}
}
fn tick(&mut self, now: std::time::Instant) {
while let Ok(msg) = self.rx.try_recv() {
match msg {
Msg::Progress(stage, ok, detail) => self.handle_progress(stage, ok, detail),
Msg::Done(status, content) => self.finish(status, content),
}
}
if let Some(n) = self.countdown.as_mut() {
let elapsed = now.duration_since(self.last_tick);
if elapsed >= Duration::from_secs(1) {
self.last_tick = now;
if *n > 0 {
*n -= 1;
}
}
}
}
}
impl eframe::App for App {
fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
if let Some(mut slot) = self.result_slot.lock().ok() {
*slot = Some(GuiResult { status: self.result.unwrap_or(false), content: self.detail.clone() });
}
}
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
let ctx = ui.ctx().clone();
self.tick(std::time::Instant::now());
if self.countdown == Some(0) {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
return;
}
if self.countdown.is_some() {
ctx.request_repaint_after(Duration::from_millis(200));
}
egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
ui.add_space(8.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new("显卡功能性测试").size(20.0).strong());
});
ui.add_space(8.0);
egui::Frame::group(ui.style()).show(ui, |ui| {
ui.set_width(ui.available_width());
for (_, label, state) in &self.steps {
ui.horizontal(|ui| {
let (icon, color) = match state {
StepState::Waiting => ("⏸", Color32::GRAY),
StepState::Running => ("⏳", Color32::YELLOW),
StepState::Ok(_) => ("✅", Color32::from_rgb(0x2E, 0x9B, 0xE8)),
StepState::Fail(_) => ("❌", Color32::from_rgb(0xD9, 0x30, 0x40)),
StepState::Skipped => ("⏭", Color32::GRAY),
};
ui.label(RichText::new(icon).size(15.0).color(color));
ui.label(RichText::new(*label).strong());
let detail = match state {
StepState::Waiting => "等待中...".to_string(),
StepState::Running => "检测中...".to_string(),
StepState::Ok(d) | StepState::Fail(d) => d.clone(),
StepState::Skipped => "跳过(不适用)".to_string(),
};
ui.label(RichText::new(detail).color(color));
});
}
});
ui.add_space(8.0);
if !self.detail.is_empty() {
egui::Frame::group(ui.style()).show(ui, |ui| {
ui.set_width(ui.available_width());
let mut text = self.detail.clone();
ui.add(
egui::TextEdit::multiline(&mut text)
.font(egui::TextStyle::Monospace)
.desired_rows(8)
.interactive(false),
);
});
ui.add_space(8.0);
}
ui.vertical_centered(|ui| {
let (text, color) = match self.result {
None => ("检测中...".to_string(), Color32::GRAY),
Some(true) => ("PASS".to_string(), Color32::from_rgb(0x2E, 0x9B, 0xE8)),
Some(false) => ("FAIL".to_string(), Color32::from_rgb(0xD9, 0x30, 0x40)),
};
ui.label(RichText::new(text).size(30.0).strong().color(color));
});
ui.add_space(8.0);
ui.vertical_centered(|ui| {
match (&self.result, self.opts.auto) {
(Some(_), true) => {
let n = self.countdown.unwrap_or(0);
ui.label(RichText::new(format!("检测完成,{n} 秒后自动关闭...")).color(Color32::GRAY));
}
(Some(_), false) => {
if ui.add_sized([140.0, 34.0], egui::Button::new(RichText::new("确认").size(16.0).strong())).clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
}
(None, _) => {
ui.label(RichText::new("正在检测,请稍候...").color(Color32::GRAY));
}
}
});
ui.add_space(8.0);
});
}
}
fn spawn_detect(opts: &GuiOptions, tx: Sender<Msg>) {
let samples = opts.samples;
let tx_done = tx.clone();
std::thread::spawn(move || {
let cb: detect::ProgressCb = Box::new(move |stage, ok, detail| {
let _ = tx.send(Msg::Progress(stage, ok, detail));
});
let (status, content) = detect::detect(samples, Some(cb));
let _ = tx_done.send(Msg::Done(status, content));
});
}
fn load_cjk_font(ctx: &egui::Context) {
let candidates = [
"C:/Windows/Fonts/msyh.ttc",
"C:/Windows/Fonts/simhei.ttf",
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
];
for path in candidates {
if let Ok(data) = std::fs::read(path) {
let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert("cjk".into(), egui::FontData::from_owned(data).into());
fonts.families.get_mut(&egui::FontFamily::Proportional).unwrap().insert(0, "cjk".into());
ctx.set_fonts(fonts);
return;
}
}
}
pub fn run_gui(opts: GuiOptions) -> GuiResult {
let (tx, rx) = mpsc::channel();
spawn_detect(&opts, tx.clone());
let result_slot: std::sync::Arc<std::sync::Mutex<Option<GuiResult>>> = Default::default();
let slot = result_slot.clone();
let app = App::new(opts.clone(), rx, slot);
let native_options = eframe::NativeOptions {
renderer: eframe::Renderer::Glow,
viewport: egui::ViewportBuilder::default()
.with_inner_size([720.0, 600.0])
.with_min_inner_size([560.0, 480.0])
.with_title("显卡功能性测试")
.with_resizable(true),
..Default::default()
};
let _ = eframe::run_native(
"gpu-test",
native_options,
Box::new(move |cc| {
load_cjk_font(&cc.egui_ctx);
cc.egui_ctx.set_theme(egui::Theme::Dark);
Ok(Box::new(app))
}),
);
result_slot.lock().unwrap().take().unwrap_or(GuiResult { status: false, content: "GUI 未完成即关闭".into() })
}