Files
guba-indicator/rust/src/ui.rs

184 lines
5.9 KiB
Rust

use egui::{Color32, FontId, Stroke, Vec2};
use std::sync::atomic::{AtomicBool, AtomicI32};
use std::sync::Arc;
use parking_lot::Mutex;
pub struct AppState {
pub running: Arc<AtomicBool>,
pub current_score: Arc<AtomicI32>,
pub status_text: Arc<Mutex<String>>,
pub fetch_count: Arc<AtomicI32>,
pub analysis_count: Arc<AtomicI32>,
pub no_content_count: Arc<AtomicI32>,
}
impl AppState {
pub fn new() -> Self {
Self {
running: Arc::new(AtomicBool::new(false)),
current_score: Arc::new(AtomicI32::new(50)),
status_text: Arc::new(Mutex::new("就绪".to_string())),
fetch_count: Arc::new(AtomicI32::new(0)),
analysis_count: Arc::new(AtomicI32::new(0)),
no_content_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(21, 101, 192), // 深蓝
31..=38 => Color32::from_rgb(25, 118, 210), // 蓝色
39..=44 => Color32::from_rgb(66, 165, 245), // 浅蓝
45..=55 => Color32::from_rgb(102, 187, 106), // 绿色
56..=64 => Color32::from_rgb(255, 167, 38), // 橙色
65..=69 => Color32::from_rgb(251, 140, 0), // 深橙
70..=100 => Color32::from_rgb(229, 57, 53), // 红色
_ => 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 get_score_description(score: i32) -> &'static str {
match score {
0..=29 => "极度悲观",
30..=38 => "悲观",
39..=44 => "偏悲观",
45..=55 => "中立",
56..=64 => "偏乐观",
65..=69 => "乐观",
70..=100 => "极度乐观",
_ => "无法判断",
}
}
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 description = get_score_description(score);
let size = Vec2::new(140.0, 160.0);
let (rect, _response) = ui.allocate_exact_size(size, egui::Sense::hover());
let painter = ui.painter();
let center = rect.center();
// 外圈背景 - 白色主题
painter.circle_filled(center, 65.0, Color32::from_rgb(240, 240, 245));
painter.circle_stroke(center, 65.0, Stroke::new(3.0, Color32::from_rgb(200, 200, 210)));
// 内圈颜色
painter.circle_filled(center, 58.0, color);
// 发光效果
for i in 1..=3 {
let alpha = (60 / i) as u8;
let glow_color = Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), alpha);
painter.circle_filled(center, 58.0 - (i as f32) * 5.0, glow_color);
}
// 中心白色背景
painter.circle_filled(center, 45.0, Color32::from_rgb(255, 255, 255));
// 分数文本 - 深色
let score_text = format!("{}", score);
painter.text(
center - Vec2::new(0.0, 5.0),
egui::Align2::CENTER_CENTER,
score_text,
FontId::proportional(42.0),
color,
);
// 标签文本 - 深色
painter.text(
center + Vec2::new(0.0, 28.0),
egui::Align2::CENTER_CENTER,
label,
FontId::proportional(16.0),
Color32::from_rgb(50, 50, 55),
);
// 描述文本(在指示灯下方)
ui.add_space(10.0);
ui.label(egui::RichText::new(description).size(14.0).color(Color32::from_rgb(100, 100, 110)));
}
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, 8.0, Color32::from_rgb(255, 255, 255));
painter.rect_stroke(rect, 8.0, Stroke::new(1.0, Color32::from_rgb(220, 220, 230)));
if data.is_empty() {
let text = "暂无数据";
painter.text(
rect.center(),
egui::Align2::CENTER_CENTER,
text,
FontId::proportional(14.0),
Color32::from_rgb(150, 150, 160),
);
return;
}
let values: Vec<f64> = 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 = 15.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..=4 {
let y = rect.max.y - padding - (i as f32) * (draw_height / 4.0);
painter.line_segment(
[egui::pos2(rect.min.x + padding, y), egui::pos2(rect.max.x - padding, y)],
Stroke::new(1.0, Color32::from_rgba_unmultiplied(200, 200, 210, 100)),
);
}
// 绘制数据线
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, 123, 255)),
);
}
// 绘制数据点
for i in 0..data.len() {
let x = rect.min.x + padding + (i as f32) * step_x;
let y = rect.max.y - padding - ((values[i] - min_val) / range * draw_height as f64) as f32;
painter.circle_filled(egui::pos2(x, y), 3.0, Color32::from_rgb(0, 150, 255));
}
}