nightshade-editor 0.13.4

An interactive editor for the Nightshade game engine
use nightshade::prelude::egui;
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,
        }
    }
}

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()
}