use egui::{Color32, CornerRadius, FontId, RichText, Style, TextStyle, Vec2, Visuals};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Theme {
#[default]
Dark,
Light,
}
impl Theme {
pub const fn as_str(self) -> &'static str {
match self {
Self::Dark => "dark",
Self::Light => "light",
}
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Self {
match s.trim().to_ascii_lowercase().as_str() {
"light" | "浅色" => Self::Light,
_ => Self::Dark,
}
}
pub const fn flipped(self) -> Self {
match self {
Self::Dark => Self::Light,
Self::Light => Self::Dark,
}
}
pub const fn as_u8(self) -> u8 {
match self {
Self::Dark => 0,
Self::Light => 1,
}
}
pub const fn from_u8(v: u8) -> Self {
match v {
1 => Self::Light,
_ => Self::Dark,
}
}
pub const fn palette(self) -> Palette {
match self {
Self::Dark => PALETTE_DARK,
Self::Light => PALETTE_LIGHT,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Palette {
pub bg1: Color32,
pub bg2: Color32,
pub bg4: Color32,
pub bg5: Color32,
pub bg6: Color32,
pub accent2: Color32,
pub accent4: Color32,
pub border: Color32,
pub border_soft: Color32,
pub text1: Color32,
pub text2: Color32,
pub text3: Color32,
}
pub const PALETTE_DARK: Palette = Palette {
bg1: shadcn::BG1,
bg2: shadcn::BG2,
bg4: shadcn::BG4,
bg5: shadcn::BG5,
bg6: shadcn::BG6,
accent2: shadcn::ACCENT2,
accent4: shadcn::ACCENT4,
border: shadcn::BORDER,
border_soft: shadcn::BORDER_SOFT,
text1: shadcn::TEXT1,
text2: shadcn::TEXT2,
text3: shadcn::TEXT3,
};
pub const PALETTE_LIGHT: Palette = Palette {
bg1: Color32::from_rgb(0xF9, 0xFA, 0xFB),
bg2: Color32::from_rgb(0xFF, 0xFF, 0xFF),
bg4: Color32::from_rgb(0xE9, 0xED, 0xF2),
bg5: Color32::from_rgb(0xDD, 0xE4, 0xEC),
bg6: Color32::from_rgb(0xD3, 0xDB, 0xE6),
accent2: Color32::from_rgb(0xE8, 0xED, 0xF4),
accent4: Color32::from_rgb(0x40, 0x5D, 0xE6),
border: Color32::from_rgb(0xD5, 0xDD, 0xE8),
border_soft: Color32::from_rgb(0xEE, 0xEE, 0xEE),
text1: Color32::from_rgb(0x22, 0x1E, 0x1F),
text2: Color32::from_rgb(0x30, 0x30, 0x30),
text3: Color32::from_rgb(0x5B, 0x61, 0x69),
};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Semantic {
pub rise: Color32,
pub fall: Color32,
pub warn: Color32,
pub error: Color32,
pub ok: Color32,
pub info: Color32,
pub muted: Color32,
}
pub const SEMANTIC_DARK: Semantic = Semantic {
rise: semantic::RISE,
fall: semantic::FALL,
warn: semantic::WARN,
error: semantic::ERROR,
ok: semantic::OK,
info: semantic::INFO,
muted: semantic::MUTED,
};
pub const SEMANTIC_LIGHT: Semantic = Semantic {
rise: Color32::from_rgb(0xC0, 0x30, 0x30),
fall: Color32::from_rgb(0x1F, 0x7A, 0x1F),
warn: Color32::from_rgb(0x8A, 0x5A, 0x00),
error: Color32::from_rgb(0xB0, 0x1A, 0x2E),
ok: Color32::from_rgb(0x16, 0x65, 0x34),
info: Color32::from_rgb(0x06, 0x68, 0xC2),
muted: Color32::from_rgb(0x6F, 0x6F, 0x6F),
};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AlertSkin {
pub info: Color32,
pub warning: Color32,
pub error: Color32,
pub info_tint: Color32,
pub warning_tint: Color32,
pub error_tint: Color32,
pub info_border: Color32,
pub warning_border: Color32,
pub error_border: Color32,
}
pub const ALERT_DARK: AlertSkin = AlertSkin {
info: alert::INFO,
warning: alert::WARNING,
error: alert::ERROR,
info_tint: shadcn::BG2,
warning_tint: shadcn::BG2,
error_tint: shadcn::BG2,
info_border: alert::INFO,
warning_border: alert::WARNING,
error_border: alert::ERROR,
};
pub const ALERT_LIGHT: AlertSkin = AlertSkin {
info: Color32::from_rgb(0x06, 0x68, 0xC2),
warning: Color32::from_rgb(0x8A, 0x5A, 0x00),
error: Color32::from_rgb(0xB0, 0x1A, 0x2E),
info_tint: Color32::from_rgb(0xE8, 0xF4, 0xFF),
warning_tint: Color32::from_rgb(0xFF, 0xF6, 0xE0),
error_tint: Color32::from_rgb(0xFD, 0xEC, 0xEE),
info_border: Color32::from_rgb(0xB9, 0xDC, 0xF7),
warning_border: Color32::from_rgb(0xF0, 0xD9, 0xA0),
error_border: Color32::from_rgb(0xF5, 0xB8, 0xC2),
};
fn theme_key() -> egui::Id {
egui::Id::new("ek-ui.theme")
}
pub fn theme_of(ctx: &egui::Context) -> Theme {
ctx.data(|d| d.get_temp::<Theme>(theme_key()))
.unwrap_or_default()
}
pub fn set_theme(ctx: &egui::Context, theme: Theme) {
ctx.data_mut(|d| d.insert_temp(theme_key(), theme));
}
pub fn palette(ctx: &egui::Context) -> Palette {
theme_of(ctx).palette()
}
pub fn semantic_of(ctx: &egui::Context) -> Semantic {
semantic_of_theme(theme_of(ctx))
}
pub const fn semantic_of_theme(theme: Theme) -> Semantic {
match theme {
Theme::Dark => SEMANTIC_DARK,
Theme::Light => SEMANTIC_LIGHT,
}
}
pub const fn alert_of_theme(theme: Theme) -> AlertSkin {
match theme {
Theme::Dark => ALERT_DARK,
Theme::Light => ALERT_LIGHT,
}
}
pub fn rise(ctx: &egui::Context) -> Color32 {
semantic_of(ctx).rise
}
pub fn fall(ctx: &egui::Context) -> Color32 {
semantic_of(ctx).fall
}
pub fn warn(ctx: &egui::Context) -> Color32 {
semantic_of(ctx).warn
}
pub fn error(ctx: &egui::Context) -> Color32 {
semantic_of(ctx).error
}
pub fn ok(ctx: &egui::Context) -> Color32 {
semantic_of(ctx).ok
}
pub fn info(ctx: &egui::Context) -> Color32 {
semantic_of(ctx).info
}
pub fn muted(ctx: &egui::Context) -> Color32 {
semantic_of(ctx).muted
}
pub mod shadcn {
use super::*;
pub const BG1: Color32 = Color32::from_rgb(0x0E, 0x11, 0x13);
pub const BG2: Color32 = Color32::from_rgb(0x16, 0x1A, 0x1D);
pub const BG4: Color32 = Color32::from_rgb(0x1D, 0x22, 0x26);
pub const BG5: Color32 = Color32::from_rgb(0x2A, 0x30, 0x36);
pub const BG6: Color32 = Color32::from_rgb(0x34, 0x3B, 0x42);
pub const TEXT1: Color32 = Color32::from_rgb(0xEE, 0xF1, 0xF3);
pub const TEXT2: Color32 = Color32::from_rgb(0xC7, 0xCD, 0xD3);
pub const TEXT3: Color32 = Color32::from_rgb(0x8E, 0x97, 0x9F);
pub const ACCENT2: Color32 = Color32::from_rgb(0x34, 0x3B, 0x42);
pub const ACCENT4: Color32 = Color32::from_rgb(0xE5, 0xE5, 0xE5);
pub const BORDER: Color32 = Color32::from_rgb(0x2A, 0x30, 0x36);
pub const BORDER_SOFT: Color32 = Color32::from_rgb(0x22, 0x27, 0x2C);
}
pub mod alert {
use egui::Color32;
pub const INFO: Color32 = Color32::from_rgb(0x18, 0x90, 0xFF);
pub const WARNING: Color32 = Color32::from_rgb(0xFA, 0xAD, 0x14);
pub const ERROR: Color32 = Color32::from_rgb(0xFF, 0x4D, 0x4F);
}
pub mod semantic {
use super::*;
pub const RISE: Color32 = Color32::from_rgb(0xF0, 0x75, 0x75);
pub const FALL: Color32 = Color32::from_rgb(0x42, 0xBB, 0x46);
pub const WARN: Color32 = Color32::from_rgb(0xE0, 0x88, 0x10);
pub const ERROR: Color32 = Color32::from_rgb(0xDC, 0x60, 0x60);
pub const OK: Color32 = Color32::from_rgb(0x54, 0xAC, 0x5C);
pub const INFO: Color32 = Color32::from_rgb(0x9A, 0xA8, 0xC0);
pub const MUTED: Color32 = Color32::from_rgb(0x94, 0x94, 0x94);
}
pub const FONT_HEADING: f32 = 22.0;
pub const FONT_BODY: f32 = 16.0;
pub const FONT_SMALL: f32 = 14.0;
pub const FONT_MONO: f32 = 15.0;
pub const CELL_INSET_X: f32 = 6.0;
pub const CORNER_CARD: u8 = 10;
pub const CORNER_WIDGET: u8 = 8;
pub const STROKE_WIDTH: f32 = 1.0;
pub const SPACING_ITEM_X: f32 = 8.0;
pub const SPACING_ITEM_Y: f32 = 6.0;
pub const INTERACT_HEIGHT: f32 = 26.0;
pub const SCROLL_BAR_WIDTH: f32 = 5.0;
pub const FRAME_INNER: f32 = 8.0;
pub const GROUP_GAP: f32 = 12.0;
pub const MARKET_BAND_HEIGHT: f32 = 240.0;
pub const ORDER_SIDE_WIDTH: f32 = 300.0;
pub fn num_text(s: impl Into<String>) -> RichText {
RichText::new(s).monospace()
}
pub fn apply(style: &mut Style) {
style
.text_styles
.insert(TextStyle::Body, FontId::proportional(FONT_BODY));
style
.text_styles
.insert(TextStyle::Button, FontId::proportional(FONT_BODY));
style
.text_styles
.insert(TextStyle::Small, FontId::proportional(FONT_SMALL));
style
.text_styles
.insert(TextStyle::Heading, FontId::proportional(FONT_HEADING));
style
.text_styles
.insert(TextStyle::Monospace, FontId::monospace(FONT_MONO));
style.spacing.item_spacing = Vec2::new(SPACING_ITEM_X, SPACING_ITEM_Y);
style.spacing.interact_size = Vec2::new(40.0, INTERACT_HEIGHT);
style.spacing.scroll = egui::style::ScrollStyle {
bar_width: SCROLL_BAR_WIDTH,
bar_inner_margin: 2.0,
..egui::style::ScrollStyle::solid()
};
}
pub fn visuals_for(theme: Theme) -> Visuals {
visuals_of(theme.palette(), theme)
}
pub fn visuals() -> Visuals {
visuals_of(PALETTE_DARK, Theme::Dark)
}
fn visuals_of(p: Palette, theme: Theme) -> Visuals {
let r_card = CornerRadius::same(CORNER_CARD);
let r_widget = CornerRadius::same(CORNER_WIDGET);
let mut v = match theme {
Theme::Dark => Visuals::dark(),
Theme::Light => Visuals::light(),
};
v.dark_mode = matches!(theme, Theme::Dark);
v.striped = false;
v.window_fill = p.bg1;
v.panel_fill = p.bg1;
v.extreme_bg_color = p.bg1; v.faint_bg_color = p.bg2; v.hyperlink_color = p.accent4;
v.weak_text_color = Some(p.text3);
v.window_stroke = egui::Stroke::new(STROKE_WIDTH, p.border);
v.window_corner_radius = r_card;
v.menu_corner_radius = r_card;
let stroke1 = egui::Stroke::new(STROKE_WIDTH, p.text1);
let stroke_body = egui::Stroke::new(STROKE_WIDTH, p.text2);
let border = egui::Stroke::new(STROKE_WIDTH, p.border);
v.widgets.noninteractive = egui::style::WidgetVisuals {
bg_fill: p.bg1,
weak_bg_fill: p.bg1,
bg_stroke: border, corner_radius: r_widget,
fg_stroke: stroke_body,
expansion: 0.0,
};
v.widgets.inactive = egui::style::WidgetVisuals {
bg_fill: p.bg4,
weak_bg_fill: p.bg4,
bg_stroke: border, corner_radius: r_widget,
fg_stroke: stroke1,
expansion: 0.0,
};
v.widgets.hovered = egui::style::WidgetVisuals {
bg_fill: p.bg5,
weak_bg_fill: p.bg5,
bg_stroke: border,
corner_radius: r_widget,
fg_stroke: stroke1,
expansion: 0.0,
};
v.widgets.active = egui::style::WidgetVisuals {
bg_fill: p.bg6,
weak_bg_fill: p.bg6,
bg_stroke: border,
corner_radius: r_widget,
fg_stroke: stroke1,
expansion: 0.0,
};
v.widgets.open = egui::style::WidgetVisuals {
bg_fill: p.bg6,
weak_bg_fill: p.bg6,
bg_stroke: border,
corner_radius: r_widget,
fg_stroke: stroke1,
expansion: 0.0,
};
v.selection = egui::style::Selection {
bg_fill: p.accent2,
stroke: egui::Stroke::new(STROKE_WIDTH, p.text1), };
v.interact_cursor = Some(egui::CursorIcon::PointingHand);
v
}
pub fn rise_fall(ctx: &egui::Context, v: f64) -> Color32 {
let s = semantic_of(ctx);
if v > 0.0 {
s.rise
} else if v < 0.0 {
s.fall
} else {
palette(ctx).text1
}
}