pub(crate) mod animator;
pub(crate) mod apca;
pub(crate) mod color_space;
pub(crate) mod scales;
pub mod tokens;
pub mod utils;
use animator::ColorAnimator;
use egui::{Context, Ui};
use scales::Scales;
use std::sync::Arc;
use tokens::{ColorTokens, ThemeColor};
use utils::{LABELS, THEME_NAMES, THEMES};
pub type Theme = [ThemeColor; 12];
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ApplyTo {
Global,
Local,
#[default]
ExtraScale,
}
#[derive(Debug, Default, Clone)]
pub struct Colorix {
pub tokens: ColorTokens,
pub(crate) theme: Theme,
theme_index: usize,
pub(crate) scales: Scales,
animated: bool,
pub animator: ColorAnimator,
pub(crate) apply_to: ApplyTo,
}
impl Colorix {
#[must_use]
pub fn global(ctx: &Context, theme: Theme) -> Self {
let mut colorix = Self {
theme,
..Default::default()
};
let mode = ctx.global_style().visuals.dark_mode;
colorix.apply_to = ApplyTo::Global;
colorix.tokens.apply_to = ApplyTo::Global;
colorix.set_colorix_mode(mode);
colorix.get_theme_index();
colorix.update_colors(Some(ctx), None);
colorix
}
pub fn local(ui: &mut Ui, theme: Theme) -> Self {
let mut colorix = Self {
theme,
..Default::default()
};
let mode = ui.ctx().global_style().visuals.dark_mode;
colorix.set_colorix_mode(mode);
colorix.get_theme_index();
colorix.apply_to = ApplyTo::Local;
colorix.tokens.apply_to = ApplyTo::Local;
colorix.update_colors(None, Some(ui));
colorix
}
#[must_use]
pub fn extra_scale(ctx: &Context, theme: Theme) -> Self {
let mut colorix = Self {
theme,
..Default::default()
};
let mode = ctx.global_style().visuals.dark_mode;
colorix.set_colorix_mode(mode);
colorix.get_theme_index();
colorix.apply_to = ApplyTo::ExtraScale;
colorix.tokens.apply_to = ApplyTo::ExtraScale;
colorix.update_colors(Some(ctx), None);
colorix
}
#[must_use]
pub fn local_from_style(theme: Theme, dark_mode: bool) -> Self {
let mut colorix = Self::default();
colorix.set_colorix_mode(dark_mode);
colorix.theme = theme;
colorix.apply_to = ApplyTo::Local;
colorix.tokens.apply_to = ApplyTo::Local;
colorix.update_colors(None, None);
colorix
}
#[must_use]
pub const fn animated(mut self) -> Self {
self.animated = true;
self.init_animator();
self
}
#[must_use]
pub const fn set_time(mut self, new_time: f32) -> Self {
if self.animated {
self.animator.set_time(new_time);
}
self
}
pub fn update_theme(&mut self, ctx: &egui::Context, theme: Theme) {
self.theme = theme;
self.get_theme_index();
self.update_colors(Some(ctx), None);
}
pub fn shift_tokens(&mut self, ctx: &egui::Context) {
self.animator.start(ctx);
}
#[must_use]
pub const fn dark_mode(&self) -> bool {
self.scales.dark_mode
}
const fn init_animator(&mut self) {
self.animator = ColorAnimator::new(self.tokens);
self.animator.apply_to = self.apply_to;
}
pub fn set_animator(&mut self, ctx: &Context) {
match self.apply_to {
ApplyTo::Global | ApplyTo::ExtraScale => {
if self.animated {
self.animator.set_animate(Some(ctx), None, self.tokens);
}
}
ApplyTo::Local => {}
}
}
fn get_theme_index(&mut self) {
if let Some(i) = THEMES.iter().position(|t| t == &self.theme) {
self.theme_index = i;
}
}
pub fn twelve_from_custom(&mut self, ui: &mut Ui) {
self.theme = [ThemeColor::Custom(self.scales.custom()); 12];
self.match_and_update_colors(ui);
}
fn match_and_update_colors(&mut self, ui: &mut Ui) {
match self.apply_to {
ApplyTo::Global | ApplyTo::ExtraScale => {
self.update_colors(Some(ui.ctx()), None);
}
ApplyTo::Local => {
self.update_colors(None, Some(ui));
}
}
}
const fn set_colorix_mode(&mut self, mode: bool) {
self.scales.dark_mode = mode;
self.tokens.dark_mode = mode;
}
pub fn update_locally(&mut self, ui: &mut Ui) {
if self.apply_to == ApplyTo::Local {
if self.animated {
self.animator.set_animate(None, Some(ui), self.tokens);
} else {
self.update_colors(None, Some(ui));
}
}
}
fn set_ui_mode(&self, ui: &mut Ui, mode: bool) {
match self.apply_to {
ApplyTo::Global => ui
.ctx()
.global_style_mut(|style| style.visuals.dark_mode = mode),
ApplyTo::Local => ui.style_mut().visuals.dark_mode = mode,
ApplyTo::ExtraScale => {}
}
}
pub fn set_dark(&mut self, ui: &mut Ui) {
self.set_colorix_mode(true);
self.set_ui_mode(ui, true);
self.match_and_update_colors(ui);
}
pub fn set_light(&mut self, ui: &mut Ui) {
self.set_colorix_mode(false);
self.set_ui_mode(ui, false);
self.match_and_update_colors(ui);
}
fn process_theme(&mut self) {
let mut processed: Vec<usize> = vec![];
for (i, v) in self.theme.iter().enumerate() {
if !processed.contains(&i) {
self.scales.process_color(*v);
self.tokens.update_schema(i, self.scales.scale[i]);
if i < self.theme.len() {
for (j, w) in self.theme[i + 1..].iter().enumerate() {
if w == v {
self.tokens
.update_schema(j + i + 1, self.scales.scale[j + i + 1]);
processed.push(j + i + 1);
}
}
}
}
}
}
fn match_egui_visuals(&self, ui: &mut Ui) {
match self.apply_to {
ApplyTo::Global => self.tokens.set_ctx_visuals(ui.ctx()),
ApplyTo::Local => self.tokens.set_ui_visuals(ui),
ApplyTo::ExtraScale => {}
}
}
fn update_color(&mut self, ui: &mut Ui, i: usize) {
self.scales.process_color(self.theme[i]);
self.tokens.update_schema(i, self.scales.scale[i]);
self.tokens.color_on_accent();
if self.animated {
self.animator.start(ui.ctx());
} else {
self.match_egui_visuals(ui);
}
}
fn update_colors(&mut self, ctx: Option<&Context>, ui: Option<&mut Ui>) {
if self.animated {
self.process_theme();
self.tokens.color_on_accent();
if let Some(ctx) = ctx {
self.animator.start(ctx);
} else if let Some(ui) = ui {
self.animator.start(ui.ctx());
}
} else {
self.process_theme();
self.tokens.color_on_accent();
if let Some(ctx) = ctx {
if self.apply_to != ApplyTo::ExtraScale {
self.tokens.set_ctx_visuals(ctx);
}
} else if let Some(ui) = ui {
self.tokens.set_ui_visuals(ui);
}
}
}
pub fn light_dark_toggle_button(&mut self, ui: &mut Ui, size: f32) {
#![allow(clippy::collapsible_else_if)]
if self.dark_mode() {
if ui
.add(egui::Button::new(egui::RichText::new("☀").size(size)).frame(false))
.on_hover_text("Switch to light mode")
.clicked()
{
self.set_colorix_mode(false);
self.set_ui_mode(ui, false);
self.match_and_update_colors(ui);
}
} else {
if ui
.add(egui::Button::new(egui::RichText::new("🌙").size(size)).frame(false))
.on_hover_text("Switch to dark mode")
.clicked()
{
self.set_colorix_mode(true);
self.set_ui_mode(ui, true);
self.match_and_update_colors(ui);
}
}
}
pub fn themes_dropdown(
&mut self,
ui: &mut Ui,
custom_themes: Option<(Vec<&str>, Vec<Theme>)>,
custom_only: bool,
) {
let combi_themes: Vec<Theme>;
let combi_names: Vec<&str>;
if let Some(custom) = custom_themes {
let (names, themes) = custom;
if custom_only {
combi_themes = themes;
combi_names = names;
} else {
combi_themes = THEMES.iter().copied().chain(themes).collect();
combi_names = THEME_NAMES.iter().copied().chain(names).collect();
}
} else {
combi_names = THEME_NAMES.to_vec();
combi_themes = THEMES.to_vec();
}
egui::ComboBox::from_id_salt(ui.id())
.selected_text(combi_names[self.theme_index])
.show_ui(ui, |ui| {
for i in 0..combi_themes.len() {
if ui
.selectable_value(&mut self.theme, combi_themes[i], combi_names[i])
.clicked()
{
self.theme_index = i;
self.match_and_update_colors(ui);
}
}
});
}
pub fn ui_combo_12(&mut self, ui: &mut Ui, copy: bool) {
let dropdown_colors: [ThemeColor; 23] = [
ThemeColor::Gray,
ThemeColor::EguiBlue,
ThemeColor::Tomato,
ThemeColor::Red,
ThemeColor::Ruby,
ThemeColor::Crimson,
ThemeColor::Pink,
ThemeColor::Plum,
ThemeColor::Purple,
ThemeColor::Violet,
ThemeColor::Iris,
ThemeColor::Indigo,
ThemeColor::Blue,
ThemeColor::Cyan,
ThemeColor::Teal,
ThemeColor::Jade,
ThemeColor::Green,
ThemeColor::Grass,
ThemeColor::Brown,
ThemeColor::Bronze,
ThemeColor::Gold,
ThemeColor::Orange,
ThemeColor::Custom(self.scales.custom()),
];
ui.vertical(|ui| {
for (i, label) in LABELS.iter().enumerate() {
ui.horizontal(|ui| {
let color_edit_size = egui::vec2(40.0, 18.0);
if let Some(ThemeColor::Custom(rgb)) = self.theme.get_mut(i) {
let re = ui.color_edit_button_srgb(rgb);
if re.changed() {
self.update_color(ui, i);
}
} else {
ui.add_space(color_edit_size.x + ui.style().spacing.item_spacing.x);
}
let color = if self.animated {
self.animator.animated_tokens.get_token(i)
} else {
self.tokens.get_token(i)
};
egui::widgets::color_picker::show_color(ui, color, color_edit_size);
egui::ComboBox::from_label(*label)
.selected_text(self.theme[i].label())
.show_ui(ui, |ui| {
for preset in dropdown_colors {
if ui
.selectable_value(&mut self.theme[i], preset, preset.label())
.clicked()
{
self.update_color(ui, i);
}
}
});
});
}
});
if copy {
ui.add_space(10.);
if ui.button("Copy theme to clipboard").clicked() {
ui.ctx().copy_text(format!("{:#?}", self.theme));
}
}
}
pub fn custom_picker(&mut self, ui: &mut Ui) {
if egui::color_picker::color_edit_button_hsva(
ui,
&mut self.scales.custom,
egui::color_picker::Alpha::Opaque,
)
.changed()
{
self.scales.clamp_custom();
}
}
pub fn draw_background(&mut self, ctx: &Context, accent: bool) {
let (ui_element, background) = if self.animated {
(
self.animator.animated_tokens.ui_element_background,
self.animator.animated_tokens.app_background,
)
} else {
(
self.tokens.ui_element_background,
self.tokens.app_background,
)
};
let bg = if accent {
self.animator.animated_tokens.active_ui_element_background
} else {
ui_element
};
let rect = egui::Context::content_rect(ctx);
let layer_id = egui::LayerId::background();
let painter = egui::Painter::new(ctx.clone(), layer_id, rect);
let mut mesh = egui::Mesh::default();
mesh.colored_vertex(rect.left_top(), background);
mesh.colored_vertex(rect.right_top(), background);
mesh.colored_vertex(rect.left_bottom(), bg);
mesh.colored_vertex(rect.right_bottom(), bg);
mesh.add_triangle(0, 1, 2);
mesh.add_triangle(1, 2, 3);
painter.add(egui::Shape::Mesh(Arc::new(mesh)));
}
#[must_use]
pub const fn theme(&self) -> &Theme {
&self.theme
}
}