use eframe::egui::{self, Color32}; #[derive(Clone, Copy)] pub struct AppTheme { pub bg: egui::Color32, pub panel: egui::Color32, pub panel_strong: egui::Color32, pub panel_deep: egui::Color32, pub border: egui::Color32, pub border_soft: egui::Color32, pub text: egui::Color32, pub text_dim: egui::Color32, pub text_subtle: egui::Color32, pub accent: egui::Color32, pub accent_hot: egui::Color32, pub radius: u8, } #[derive(Clone, Copy)] pub struct DesignMetrics { pub row_gap: f32, pub item_gap: f32, pub field_height: f32, pub button_height: f32, pub icon_button: egui::Vec2, pub panel_padding: i8, pub group_padding_x: i8, pub group_padding_y: i8, } pub const ONE_DARK_PRO: AppTheme = AppTheme { bg: egui::Color32::BLACK, panel: egui::Color32::from_rgb(22, 28, 35), panel_strong: egui::Color32::from_rgb(34, 43, 54), panel_deep: egui::Color32::from_rgb(15, 20, 27), border: egui::Color32::from_rgb(78, 93, 110), border_soft: egui::Color32::from_rgb(50, 62, 76), text: egui::Color32::from_rgb(235, 241, 247), text_dim: egui::Color32::from_rgb(174, 185, 198), text_subtle: egui::Color32::from_rgb(126, 139, 154), accent: egui::Color32::from_rgb(41, 192, 204), accent_hot: egui::Color32::from_rgb(247, 190, 84), radius: 5, }; pub const ACCENT_GREEN: egui::Color32 = egui::Color32::from_rgb(126, 203, 137); pub const ACCENT_RED: egui::Color32 = egui::Color32::from_rgb(240, 99, 94); pub const ACCENT_BLUE: egui::Color32 = egui::Color32::from_rgb(96, 170, 240); pub const ACCENT_ORANGE: egui::Color32 = egui::Color32::from_rgb(242, 163, 87); pub const METRICS: DesignMetrics = DesignMetrics { row_gap: 10.0, item_gap: 8.0, field_height: 24.0, button_height: 28.0, icon_button: egui::vec2(30.0, 26.0), panel_padding: 12, group_padding_x: 10, group_padding_y: 8, }; pub mod layout { pub const TITLE_BAR_HEIGHT: f32 = 36.0; pub const CENTER_PANEL_TOP: f32 = 48.0; pub const LEFT_X: f32 = 24.0; pub const RIGHT_X: f32 = 1328.0; pub const TOP_Y: f32 = 56.0; pub const LEFT_SECOND_Y: f32 = 292.0; pub const RIGHT_SECOND_Y: f32 = 388.0; pub const LEFT_TAG_X: f32 = 24.0; pub const RIGHT_TAG_X: f32 = 1810.0; } pub fn apply_theme(ctx: &egui::Context, theme: &AppTheme) { let mut visuals = egui::Visuals::dark(); visuals.override_text_color = Some(theme.text); visuals.panel_fill = theme.bg; visuals.window_fill = theme.panel; visuals.window_stroke = egui::Stroke::new(1.0, theme.border); visuals.extreme_bg_color = theme.panel_deep; visuals.faint_bg_color = theme.panel_strong; visuals.code_bg_color = theme.panel_deep; visuals.warn_fg_color = theme.accent_hot; visuals.error_fg_color = ACCENT_RED; visuals.widgets.noninteractive.bg_fill = theme.panel_strong; visuals.widgets.noninteractive.bg_stroke = egui::Stroke::new(1.0, theme.border_soft); visuals.widgets.noninteractive.fg_stroke = egui::Stroke::new(1.0, theme.text); visuals.widgets.inactive.bg_fill = theme.panel_strong; visuals.widgets.inactive.bg_stroke = egui::Stroke::new(1.0, theme.border_soft); visuals.widgets.inactive.fg_stroke = egui::Stroke::new(1.0, theme.text); visuals.widgets.hovered.bg_fill = egui::Color32::from_rgb(44, 57, 70); visuals.widgets.hovered.bg_stroke = egui::Stroke::new(1.0, theme.accent); visuals.widgets.hovered.fg_stroke = egui::Stroke::new(1.0, egui::Color32::WHITE); visuals.widgets.active.bg_fill = theme.accent; visuals.widgets.active.bg_stroke = egui::Stroke::new(1.0, theme.accent_hot); visuals.widgets.active.fg_stroke = egui::Stroke::new(1.0, egui::Color32::WHITE); visuals.widgets.open.bg_fill = egui::Color32::from_rgb(39, 50, 62); visuals.widgets.open.bg_stroke = egui::Stroke::new(1.0, theme.accent); visuals.widgets.open.fg_stroke = egui::Stroke::new(1.0, theme.text); visuals.selection.bg_fill = egui::Color32::from_rgb(35, 123, 140); visuals.selection.stroke = egui::Stroke::new(1.0, egui::Color32::WHITE); visuals.hyperlink_color = theme.accent_hot; ctx.set_visuals(visuals); let mut style = (*ctx.global_style()).clone(); style.spacing.item_spacing = egui::vec2(METRICS.item_gap, 6.0); style.spacing.button_padding = egui::vec2(12.0, 5.0); style.spacing.window_margin = egui::Margin::same(METRICS.panel_padding); style.spacing.interact_size = egui::vec2(72.0, METRICS.field_height); style.spacing.combo_width = 112.0; style.spacing.text_edit_width = 112.0; style.visuals.window_corner_radius = egui::CornerRadius::same(theme.radius); style.visuals.menu_corner_radius = egui::CornerRadius::same(4); style.visuals.window_shadow = egui::epaint::Shadow { offset: [0, 14], blur: 28, spread: 0, color: egui::Color32::from_black_alpha(135), }; ctx.set_global_style(style); } pub fn apply_fonts(ctx: &egui::Context) { let mut fonts = egui::FontDefinitions::default(); fonts.font_data.insert( "Hack-Bold".to_owned(), egui::FontData::from_static(include_bytes!("../static/Hack-Bold.ttf")).into(), ); let has_yahei = std::fs::read(r"C:\Windows\Fonts\msyh.ttc") .or_else(|_| std::fs::read(r"C:\Windows\Fonts\msyhbd.ttc")) .map(|font_data| { fonts.font_data.insert( "Microsoft-YaHei".to_owned(), egui::FontData::from_owned(font_data).into(), ); }) .is_ok(); fonts .families .entry(egui::FontFamily::Proportional) .or_default() .insert(0, "Hack-Bold".to_owned()); if has_yahei { fonts .families .entry(egui::FontFamily::Proportional) .or_default() .push("Microsoft-YaHei".to_owned()); } fonts .families .entry(egui::FontFamily::Monospace) .or_default() .insert(0, "Hack-Bold".to_owned()); if has_yahei { fonts .families .entry(egui::FontFamily::Monospace) .or_default() .push("Microsoft-YaHei".to_owned()); } ctx.set_fonts(fonts); } pub fn panel_frame(ctx: &egui::Context) -> egui::Frame { let style = ctx.global_style(); egui::Frame::window(&style) .fill(ONE_DARK_PRO.panel) .stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border)) .corner_radius(egui::CornerRadius::same(ONE_DARK_PRO.radius)) .inner_margin(egui::Margin::same(METRICS.panel_padding)) .shadow(egui::epaint::Shadow { offset: [0, 14], blur: 28, spread: 0, color: egui::Color32::from_black_alpha(135), }) } pub fn center_panel_frame() -> egui::Frame { egui::Frame::new() .fill(ONE_DARK_PRO.panel) .corner_radius(egui::CornerRadius::same(ONE_DARK_PRO.radius)) .inner_margin(egui::Margin::symmetric( METRICS.panel_padding + 2, METRICS.panel_padding, )) } pub fn group_frame() -> egui::Frame { egui::Frame::new() .fill(ONE_DARK_PRO.panel_deep) .stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border_soft)) .corner_radius(egui::CornerRadius::same(4)) .inner_margin(egui::Margin::symmetric( METRICS.group_padding_x, METRICS.group_padding_y, )) } pub fn tag_button(label: impl Into) -> egui::Button<'static> { egui::Button::new(label) .fill(ONE_DARK_PRO.panel_strong) .stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border)) .corner_radius(egui::CornerRadius::same(4)) .min_size(egui::vec2(0.0, METRICS.button_height)) } pub fn rich_tag_button( label: impl Into, color: impl Into, ) -> egui::Button<'static> { let text = egui::RichText::new(label.into()).color(color); egui::Button::new(text) .fill(ONE_DARK_PRO.panel_strong) .stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border)) .corner_radius(egui::CornerRadius::same(4)) .min_size(egui::vec2(0.0, METRICS.button_height)) } pub fn primary_button(label: impl Into) -> egui::Button<'static> { egui::Button::new(label) .fill(ONE_DARK_PRO.accent) .stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.accent_hot)) .corner_radius(egui::CornerRadius::same(4)) .min_size(egui::vec2(112.0, METRICS.button_height)) } pub fn danger_button(label: impl Into) -> egui::Button<'static> { egui::Button::new(label) .fill(ACCENT_RED) .stroke(egui::Stroke::new( 1.0, egui::Color32::from_rgb(255, 138, 126), )) .corner_radius(egui::CornerRadius::same(4)) .min_size(egui::vec2(112.0, METRICS.button_height)) } pub fn accent_button( label: impl Into, fill: egui::Color32, ) -> egui::Button<'static> { egui::Button::new(label) .fill(fill) .stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border)) .corner_radius(egui::CornerRadius::same(4)) .min_size(egui::vec2(0.0, METRICS.button_height)) } pub fn mode_button(label: &'static str, selected: bool) -> egui::Button<'static> { let fill = if selected { ONE_DARK_PRO.accent } else { ONE_DARK_PRO.panel_strong }; let stroke = if selected { ONE_DARK_PRO.accent_hot } else { ONE_DARK_PRO.border_soft }; egui::Button::new(egui::RichText::new(label).color(egui::Color32::WHITE)) .fill(fill) .stroke(egui::Stroke::new(1.0, stroke)) .corner_radius(egui::CornerRadius::same(4)) .min_size(egui::vec2(96.0, METRICS.button_height)) } pub fn icon_button<'a>(icon: impl Into, size: egui::Vec2) -> egui::Button<'a> { egui::Button::new(icon) .fill(ONE_DARK_PRO.panel_strong) .stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border)) .corner_radius(egui::CornerRadius::same(4)) .min_size(size) } pub fn panel_title(title: &'static str) -> egui::RichText { egui::RichText::new(title) .color(ONE_DARK_PRO.text_dim) .size(13.0) } pub fn field_label(label: impl Into) -> egui::RichText { egui::RichText::new(label.into()) .color(ONE_DARK_PRO.text_dim) .size(12.0) } pub fn value_text(text: impl Into) -> egui::RichText { egui::RichText::new(text.into()) .color(ONE_DARK_PRO.text) .size(12.0) } pub fn subtle_text(text: impl Into) -> egui::RichText { egui::RichText::new(text.into()) .color(ONE_DARK_PRO.text_subtle) .size(12.0) } pub fn dim_text() -> egui::Color32 { ONE_DARK_PRO.text_dim } pub fn accent_text() -> egui::Color32 { ONE_DARK_PRO.accent_hot } pub fn status_color_ok() -> egui::Color32 { ACCENT_GREEN } pub fn status_color_warn() -> egui::Color32 { ONE_DARK_PRO.accent_hot } pub fn status_color_error() -> egui::Color32 { ACCENT_RED } pub fn status_color_info() -> egui::Color32 { ACCENT_BLUE }