use egui::{Color32, RichText, Stroke, Vec2}; use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; use std::sync::Arc; pub struct AppState { pub running: Arc, pub current_score: Arc, pub status_text: Arc>, pub fetch_count: Arc, pub analysis_count: Arc, } impl AppState { pub fn new() -> Self { Self { running: Arc::new(AtomicBool::new(false)), current_score: Arc::new(AtomicI32::new(50)), status_text: Arc::new(parking_lot::Mutex::new("就绪".to_string())), fetch_count: Arc::new(AtomicI32::new(0)), analysis_count: Arc::new(AtomicI32::new(0)), } } } impl Default for AppState { fn default() -> Self { Self::new() } } pub fn get_score_color(score: i32) -> Color32 { match score { 0..=30 => Color32::from_rgb(0, 100, 200), 31..=50 => Color32::from_rgb(100, 200, 100), 51..=70 => Color32::from_rgb(255, 200, 0), 71..=100 => Color32::from_rgb(255, 50, 50), _ => Color32::GRAY, } } pub fn get_score_label(score: i32, cold: i32, warm: i32) -> &'static str { if score < cold { "看跌" } else if score > warm { "看涨" } else { "中性" } } pub fn draw_indicator(ui: &mut egui::Ui, score: i32, cold: i32, warm: i32) { let color = get_score_color(score); let label = get_score_label(score, cold, warm); let size = Vec2::new(120.0, 120.0); let (rect, _response) = ui.allocate_exact_size(size, egui::Sense::hover()); let painter = ui.painter(); painter.circle_filled(rect.center(), 55.0, Color32::from_rgba_unmultiplied(30, 30, 30, 200)); painter.circle_stroke(rect.center(), 55.0, Stroke::new(3.0, color)); let inner_radius = 40.0; let angle = (score as f32 / 100.0) * std::f32::consts::TAU - std::f32::consts::FRAC_PI_2; let indicator_pos = rect.center() + Vec2::new( angle.cos() * inner_radius, angle.sin() * inner_radius, ); painter.circle_filled(indicator_pos, 8.0, color); let text = RichText::new(format!("{}", score)) .heading() .size(36.0) .color(color); painter.text( rect.center() - Vec2::new(0.0, 10.0), egui::Align2::CENTER_CENTER, text, ); let label_text = RichText::new(label).size(16.0).color(Color32::WHITE); painter.text( rect.center() + Vec2::new(0.0, 25.0), egui::Align2::CENTER_CENTER, label_text, ); } pub fn draw_waveform(ui: &mut egui::Ui, data: &[(String, f64)], width: f32, height: f32) { let rect = ui.available_rect_before_wrap(); let painter = ui.painter(); painter.rect_filled(rect, 0.0, Color32::from_rgba_unmultiplied(20, 20, 30, 180)); painter.rect_stroke(rect, 0.0, Stroke::new(1.0, Color32::GRAY)); if data.is_empty() { let text = RichText::new("暂无数据").small().color(Color32::GRAY); painter.text(rect.center(), egui::Align2::CENTER_CENTER, text); return; } let values: Vec = data.iter().map(|(_, v)| *v).collect(); let min_val = values.iter().cloned().fold(f64::INFINITY, f64::min); let max_val = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max); let range = if (max_val - min_val).abs() < 0.01 { 1.0 } else { max_val - min_val }; let padding = 10.0; let draw_width = rect.width() - padding * 2.0; let draw_height = rect.height() - padding * 2.0; let step_x = if data.len() > 1 { draw_width / (data.len() - 1) as f32 } else { 0.0 }; for i in 0..data.len().saturating_sub(1) { let x1 = rect.min.x + padding + (i as f32) * step_x; let x2 = rect.min.x + padding + ((i + 1) as f32) * step_x; let y1 = rect.max.y - padding - ((values[i] - min_val) / range * draw_height as f64) as f32; let y2 = rect.max.y - padding - ((values[i + 1] - min_val) / range * draw_height as f64) as f32; painter.line_segment( [egui::pos2(x1, y1), egui::pos2(x2, y2)], Stroke::new(2.0, Color32::from_rgb(0, 150, 255)), ); } }