use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize, Deserialize)]
pub struct ThemeConfig {
pub name: String,
pub dark_mode: bool,
pub override_text_color: Option<[u8; 4]>,
pub hyperlink_color: Option<[u8; 4]>,
pub faint_bg_color: Option<[u8; 4]>,
pub extreme_bg_color: Option<[u8; 4]>,
pub code_bg_color: Option<[u8; 4]>,
pub warn_fg_color: Option<[u8; 4]>,
pub error_fg_color: Option<[u8; 4]>,
pub window_fill: Option<[u8; 4]>,
pub panel_fill: Option<[u8; 4]>,
pub window_stroke_color: Option<[u8; 4]>,
pub window_stroke_width: Option<f32>,
pub window_corner_radius: Option<f32>,
pub window_shadow_color: Option<[u8; 4]>,
pub window_shadow_size: Option<f32>,
pub popup_shadow_size: Option<f32>,
pub selection_bg_fill: Option<[u8; 4]>,
pub selection_stroke_color: Option<[u8; 4]>,
pub selection_stroke_width: Option<f32>,
pub noninteractive_bg_fill: Option<[u8; 4]>,
pub noninteractive_weak_bg_fill: Option<[u8; 4]>,
pub noninteractive_bg_stroke_color: Option<[u8; 4]>,
pub noninteractive_bg_stroke_width: Option<f32>,
pub noninteractive_corner_radius: Option<f32>,
pub noninteractive_fg_stroke_color: Option<[u8; 4]>,
pub noninteractive_fg_stroke_width: Option<f32>,
pub noninteractive_expansion: Option<f32>,
pub inactive_bg_fill: Option<[u8; 4]>,
pub inactive_weak_bg_fill: Option<[u8; 4]>,
pub inactive_bg_stroke_color: Option<[u8; 4]>,
pub inactive_bg_stroke_width: Option<f32>,
pub inactive_corner_radius: Option<f32>,
pub inactive_fg_stroke_color: Option<[u8; 4]>,
pub inactive_fg_stroke_width: Option<f32>,
pub inactive_expansion: Option<f32>,
pub hovered_bg_fill: Option<[u8; 4]>,
pub hovered_weak_bg_fill: Option<[u8; 4]>,
pub hovered_bg_stroke_color: Option<[u8; 4]>,
pub hovered_bg_stroke_width: Option<f32>,
pub hovered_corner_radius: Option<f32>,
pub hovered_fg_stroke_color: Option<[u8; 4]>,
pub hovered_fg_stroke_width: Option<f32>,
pub hovered_expansion: Option<f32>,
pub active_bg_fill: Option<[u8; 4]>,
pub active_weak_bg_fill: Option<[u8; 4]>,
pub active_bg_stroke_color: Option<[u8; 4]>,
pub active_bg_stroke_width: Option<f32>,
pub active_corner_radius: Option<f32>,
pub active_fg_stroke_color: Option<[u8; 4]>,
pub active_fg_stroke_width: Option<f32>,
pub active_expansion: Option<f32>,
pub open_bg_fill: Option<[u8; 4]>,
pub open_weak_bg_fill: Option<[u8; 4]>,
pub open_bg_stroke_color: Option<[u8; 4]>,
pub open_bg_stroke_width: Option<f32>,
pub open_corner_radius: Option<f32>,
pub open_fg_stroke_color: Option<[u8; 4]>,
pub open_fg_stroke_width: Option<f32>,
pub open_expansion: Option<f32>,
pub resize_corner_size: Option<f32>,
pub text_cursor_width: Option<f32>,
pub clip_rect_margin: Option<f32>,
pub button_frame: Option<bool>,
pub collapsing_header_frame: Option<bool>,
pub indent_has_left_vline: Option<bool>,
pub striped: Option<bool>,
pub slider_trailing_fill: Option<bool>,
}
impl Default for ThemeConfig {
fn default() -> Self {
Self {
name: "Dark".to_string(),
dark_mode: true,
override_text_color: None,
hyperlink_color: None,
faint_bg_color: None,
extreme_bg_color: None,
code_bg_color: None,
warn_fg_color: None,
error_fg_color: None,
window_fill: None,
panel_fill: None,
window_stroke_color: None,
window_stroke_width: None,
window_corner_radius: None,
window_shadow_color: None,
window_shadow_size: None,
popup_shadow_size: None,
selection_bg_fill: None,
selection_stroke_color: None,
selection_stroke_width: None,
noninteractive_bg_fill: None,
noninteractive_weak_bg_fill: None,
noninteractive_bg_stroke_color: None,
noninteractive_bg_stroke_width: None,
noninteractive_corner_radius: None,
noninteractive_fg_stroke_color: None,
noninteractive_fg_stroke_width: None,
noninteractive_expansion: None,
inactive_bg_fill: None,
inactive_weak_bg_fill: None,
inactive_bg_stroke_color: None,
inactive_bg_stroke_width: None,
inactive_corner_radius: None,
inactive_fg_stroke_color: None,
inactive_fg_stroke_width: None,
inactive_expansion: None,
hovered_bg_fill: None,
hovered_weak_bg_fill: None,
hovered_bg_stroke_color: None,
hovered_bg_stroke_width: None,
hovered_corner_radius: None,
hovered_fg_stroke_color: None,
hovered_fg_stroke_width: None,
hovered_expansion: None,
active_bg_fill: None,
active_weak_bg_fill: None,
active_bg_stroke_color: None,
active_bg_stroke_width: None,
active_corner_radius: None,
active_fg_stroke_color: None,
active_fg_stroke_width: None,
active_expansion: None,
open_bg_fill: None,
open_weak_bg_fill: None,
open_bg_stroke_color: None,
open_bg_stroke_width: None,
open_corner_radius: None,
open_fg_stroke_color: None,
open_fg_stroke_width: None,
open_expansion: None,
resize_corner_size: None,
text_cursor_width: None,
clip_rect_margin: None,
button_frame: None,
collapsing_header_frame: None,
indent_has_left_vline: None,
striped: None,
slider_trailing_fill: None,
}
}
}
pub(crate) fn rgba(color: [u8; 4]) -> egui::Color32 {
egui::Color32::from_rgba_unmultiplied(color[0], color[1], color[2], color[3])
}
pub(crate) fn to_rgba(color: egui::Color32) -> [u8; 4] {
[color.r(), color.g(), color.b(), color.a()]
}
struct WidgetOverrides {
bg_fill: Option<[u8; 4]>,
weak_bg_fill: Option<[u8; 4]>,
bg_stroke_color: Option<[u8; 4]>,
bg_stroke_width: Option<f32>,
corner_radius: Option<f32>,
fg_stroke_color: Option<[u8; 4]>,
fg_stroke_width: Option<f32>,
expansion: Option<f32>,
}
fn apply_widget_overrides(visuals: &mut egui::style::WidgetVisuals, overrides: WidgetOverrides) {
if let Some(color) = overrides.bg_fill {
visuals.bg_fill = rgba(color);
}
if let Some(color) = overrides.weak_bg_fill {
visuals.weak_bg_fill = rgba(color);
}
if let Some(color) = overrides.bg_stroke_color {
visuals.bg_stroke.color = rgba(color);
}
if let Some(width) = overrides.bg_stroke_width {
visuals.bg_stroke.width = width;
}
if let Some(radius) = overrides.corner_radius {
visuals.corner_radius = egui::CornerRadius::same(radius as u8);
}
if let Some(color) = overrides.fg_stroke_color {
visuals.fg_stroke.color = rgba(color);
}
if let Some(width) = overrides.fg_stroke_width {
visuals.fg_stroke.width = width;
}
if let Some(value) = overrides.expansion {
visuals.expansion = value;
}
}
impl ThemeConfig {
pub fn to_visuals(&self) -> egui::Visuals {
let mut visuals = if self.dark_mode {
egui::Visuals::dark()
} else {
egui::Visuals::light()
};
if let Some(color) = self.override_text_color {
visuals.override_text_color = Some(rgba(color));
}
if let Some(color) = self.hyperlink_color {
visuals.hyperlink_color = rgba(color);
}
if let Some(color) = self.faint_bg_color {
visuals.faint_bg_color = rgba(color);
}
if let Some(color) = self.extreme_bg_color {
visuals.extreme_bg_color = rgba(color);
}
if let Some(color) = self.code_bg_color {
visuals.code_bg_color = rgba(color);
}
if let Some(color) = self.warn_fg_color {
visuals.warn_fg_color = rgba(color);
}
if let Some(color) = self.error_fg_color {
visuals.error_fg_color = rgba(color);
}
if let Some(color) = self.window_fill {
visuals.window_fill = rgba(color);
}
if let Some(color) = self.panel_fill {
visuals.panel_fill = rgba(color);
}
if let Some(color) = self.window_stroke_color {
visuals.window_stroke.color = rgba(color);
}
if let Some(width) = self.window_stroke_width {
visuals.window_stroke.width = width;
}
if let Some(radius) = self.window_corner_radius {
visuals.window_corner_radius = egui::CornerRadius::same(radius as u8);
}
if let Some(color) = self.window_shadow_color {
visuals.window_shadow.color = rgba(color);
}
if let Some(size) = self.window_shadow_size {
visuals.window_shadow.spread = size as u8;
}
if let Some(size) = self.popup_shadow_size {
visuals.popup_shadow.spread = size as u8;
}
if let Some(color) = self.selection_bg_fill {
visuals.selection.bg_fill = rgba(color);
}
if let Some(color) = self.selection_stroke_color {
visuals.selection.stroke.color = rgba(color);
}
if let Some(width) = self.selection_stroke_width {
visuals.selection.stroke.width = width;
}
apply_widget_overrides(
&mut visuals.widgets.noninteractive,
WidgetOverrides {
bg_fill: self.noninteractive_bg_fill,
weak_bg_fill: self.noninteractive_weak_bg_fill,
bg_stroke_color: self.noninteractive_bg_stroke_color,
bg_stroke_width: self.noninteractive_bg_stroke_width,
corner_radius: self.noninteractive_corner_radius,
fg_stroke_color: self.noninteractive_fg_stroke_color,
fg_stroke_width: self.noninteractive_fg_stroke_width,
expansion: self.noninteractive_expansion,
},
);
apply_widget_overrides(
&mut visuals.widgets.inactive,
WidgetOverrides {
bg_fill: self.inactive_bg_fill,
weak_bg_fill: self.inactive_weak_bg_fill,
bg_stroke_color: self.inactive_bg_stroke_color,
bg_stroke_width: self.inactive_bg_stroke_width,
corner_radius: self.inactive_corner_radius,
fg_stroke_color: self.inactive_fg_stroke_color,
fg_stroke_width: self.inactive_fg_stroke_width,
expansion: self.inactive_expansion,
},
);
apply_widget_overrides(
&mut visuals.widgets.hovered,
WidgetOverrides {
bg_fill: self.hovered_bg_fill,
weak_bg_fill: self.hovered_weak_bg_fill,
bg_stroke_color: self.hovered_bg_stroke_color,
bg_stroke_width: self.hovered_bg_stroke_width,
corner_radius: self.hovered_corner_radius,
fg_stroke_color: self.hovered_fg_stroke_color,
fg_stroke_width: self.hovered_fg_stroke_width,
expansion: self.hovered_expansion,
},
);
apply_widget_overrides(
&mut visuals.widgets.active,
WidgetOverrides {
bg_fill: self.active_bg_fill,
weak_bg_fill: self.active_weak_bg_fill,
bg_stroke_color: self.active_bg_stroke_color,
bg_stroke_width: self.active_bg_stroke_width,
corner_radius: self.active_corner_radius,
fg_stroke_color: self.active_fg_stroke_color,
fg_stroke_width: self.active_fg_stroke_width,
expansion: self.active_expansion,
},
);
apply_widget_overrides(
&mut visuals.widgets.open,
WidgetOverrides {
bg_fill: self.open_bg_fill,
weak_bg_fill: self.open_weak_bg_fill,
bg_stroke_color: self.open_bg_stroke_color,
bg_stroke_width: self.open_bg_stroke_width,
corner_radius: self.open_corner_radius,
fg_stroke_color: self.open_fg_stroke_color,
fg_stroke_width: self.open_fg_stroke_width,
expansion: self.open_expansion,
},
);
if let Some(size) = self.resize_corner_size {
visuals.resize_corner_size = size;
}
if let Some(width) = self.text_cursor_width {
visuals.text_cursor.stroke.width = width;
}
if let Some(margin) = self.clip_rect_margin {
visuals.clip_rect_margin = margin;
}
if let Some(value) = self.button_frame {
visuals.button_frame = value;
}
if let Some(value) = self.collapsing_header_frame {
visuals.collapsing_header_frame = value;
}
if let Some(value) = self.indent_has_left_vline {
visuals.indent_has_left_vline = value;
}
if let Some(value) = self.striped {
visuals.striped = value;
}
if let Some(value) = self.slider_trailing_fill {
visuals.slider_trailing_fill = value;
}
visuals
}
}
pub struct ThemeState {
pub current_config: ThemeConfig,
pub presets: Vec<ThemeConfig>,
pub selected_preset_index: Option<usize>,
pub preview_theme_index: Option<usize>,
pub show_theme_editor: bool,
}
impl Default for ThemeState {
fn default() -> Self {
let presets = ThemeConfig::all_presets().to_vec();
Self {
current_config: presets[0].clone(),
presets,
selected_preset_index: Some(0),
preview_theme_index: None,
show_theme_editor: false,
}
}
}
impl ThemeState {
pub fn select_preset_by_name(&mut self, name: &str) -> bool {
for (index, preset) in self.presets.iter().enumerate() {
if preset.name == name {
self.current_config = preset.clone();
self.selected_preset_index = Some(index);
return true;
}
}
false
}
}
pub fn apply_theme(ui_context: &egui::Context, theme_state: &ThemeState) {
ui_context.set_visuals(get_active_theme_visuals(theme_state));
}
pub fn get_active_theme_visuals(theme_state: &ThemeState) -> egui::Visuals {
if let Some(preview_index) = theme_state.preview_theme_index
&& let Some(preset) = theme_state.presets.get(preview_index)
{
return preset.to_visuals();
}
theme_state.current_config.to_visuals()
}