1pub(crate) mod animator;
13pub(crate) mod apca;
14pub(crate) mod color_space;
15pub(crate) mod scales;
16pub mod tokens;
17pub mod utils;
19
20use animator::ColorAnimator;
21use egui::{Context, Ui};
22use scales::Scales;
23use std::sync::Arc;
24use tokens::{ColorTokens, ThemeColor};
25use utils::{LABELS, THEME_NAMES, THEMES};
26
27pub type Theme = [ThemeColor; 12];
29
30#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
31pub(crate) enum ApplyTo {
32 Global,
33 Local,
34 #[default]
35 ExtraScale,
36}
37
38#[derive(Debug, Default, Clone)]
67pub struct Colorix {
68 pub tokens: ColorTokens,
69 pub(crate) theme: Theme,
70 theme_index: usize,
71 pub(crate) scales: Scales,
72 animated: bool,
73 pub animator: ColorAnimator,
74 pub(crate) apply_to: ApplyTo,
75}
76
77impl Colorix {
78 #[must_use]
79 pub fn global(ctx: &Context, theme: Theme) -> Self {
80 let mut colorix = Self {
81 theme,
82 ..Default::default()
83 };
84 let mode = ctx.style().visuals.dark_mode;
85 colorix.apply_to = ApplyTo::Global;
86 colorix.tokens.apply_to = ApplyTo::Global;
87 colorix.set_colorix_mode(mode);
88 colorix.get_theme_index();
89 colorix.update_colors(Some(ctx), None);
90 colorix
91 }
92 pub fn local(ui: &mut Ui, theme: Theme) -> Self {
95 let mut colorix = Self {
96 theme,
97 ..Default::default()
98 };
99 let mode = ui.ctx().style().visuals.dark_mode;
100 colorix.set_colorix_mode(mode);
101 colorix.get_theme_index();
102 colorix.apply_to = ApplyTo::Local;
103 colorix.tokens.apply_to = ApplyTo::Local;
104 colorix.update_colors(None, Some(ui));
105 colorix
106 }
107 #[must_use]
109 pub fn extra_scale(ctx: &Context, theme: Theme) -> Self {
110 let mut colorix = Self {
111 theme,
112 ..Default::default()
113 };
114 let mode = ctx.style().visuals.dark_mode;
115 colorix.set_colorix_mode(mode);
116 colorix.get_theme_index();
117 colorix.apply_to = ApplyTo::ExtraScale;
118 colorix.tokens.apply_to = ApplyTo::ExtraScale;
119 colorix.update_colors(Some(ctx), None);
120 colorix
121 }
122 #[must_use]
123 pub fn local_from_style(theme: Theme, dark_mode: bool) -> Self {
124 let mut colorix = Self::default();
125 colorix.set_colorix_mode(dark_mode);
126 colorix.theme = theme;
127 colorix.apply_to = ApplyTo::Local;
128 colorix.tokens.apply_to = ApplyTo::Local;
129 colorix.update_colors(None, None);
130 colorix
131 }
132 #[must_use]
148 pub const fn animated(mut self) -> Self {
149 self.animated = true;
150 self.init_animator();
151 self
152 }
153 #[must_use]
155 pub const fn set_time(mut self, new_time: f32) -> Self {
156 if self.animated {
157 self.animator.set_time(new_time);
158 }
159 self
160 }
161
162 pub fn update_theme(&mut self, ctx: &egui::Context, theme: Theme) {
164 self.theme = theme;
165 self.get_theme_index();
166 self.update_colors(Some(ctx), None);
167 }
168 pub fn shift_tokens(&mut self, ctx: &egui::Context) {
170 self.animator.start(ctx);
171 }
172 #[must_use]
173 pub const fn dark_mode(&self) -> bool {
174 self.scales.dark_mode
175 }
176
177 const fn init_animator(&mut self) {
178 self.animator = ColorAnimator::new(self.tokens);
179 self.animator.apply_to = self.apply_to;
180 }
181
182 pub fn set_animator(&mut self, ctx: &Context) {
185 match self.apply_to {
186 ApplyTo::Global | ApplyTo::ExtraScale => {
187 if self.animated {
188 self.animator.set_animate(Some(ctx), None, self.tokens);
189 }
190 }
191 ApplyTo::Local => {}
192 }
193 }
194
195 fn get_theme_index(&mut self) {
196 if let Some(i) = THEMES.iter().position(|t| t == &self.theme) {
197 self.theme_index = i;
198 }
199 }
200 pub fn twelve_from_custom(&mut self, ui: &mut Ui) {
202 self.theme = [ThemeColor::Custom(self.scales.custom()); 12];
203 self.match_and_update_colors(ui);
204 }
205
206 fn match_and_update_colors(&mut self, ui: &mut Ui) {
207 match self.apply_to {
208 ApplyTo::Global | ApplyTo::ExtraScale => {
209 self.update_colors(Some(ui.ctx()), None);
210 }
211 ApplyTo::Local => {
212 self.update_colors(None, Some(ui));
213 }
214 }
215 }
216 const fn set_colorix_mode(&mut self, mode: bool) {
217 self.scales.dark_mode = mode;
218 self.tokens.dark_mode = mode;
219 }
220 pub fn update_locally(&mut self, ui: &mut Ui) {
223 if self.apply_to == ApplyTo::Local {
224 if self.animated {
225 self.animator.set_animate(None, Some(ui), self.tokens);
226 } else {
227 self.update_colors(None, Some(ui));
228 }
229 }
230 }
231
232 fn set_ui_mode(&self, ui: &mut Ui, mode: bool) {
233 match self.apply_to {
234 ApplyTo::Global => ui.ctx().style_mut(|style| style.visuals.dark_mode = mode),
235 ApplyTo::Local => ui.style_mut().visuals.dark_mode = mode,
236 ApplyTo::ExtraScale => {}
237 }
238 }
239 pub fn set_dark(&mut self, ui: &mut Ui) {
241 self.set_colorix_mode(true);
242 self.set_ui_mode(ui, true);
243 self.match_and_update_colors(ui);
244 }
245 pub fn set_light(&mut self, ui: &mut Ui) {
247 self.set_colorix_mode(false);
248 self.set_ui_mode(ui, false);
249 self.match_and_update_colors(ui);
250 }
251
252 fn process_theme(&mut self) {
253 let mut processed: Vec<usize> = vec![];
254 for (i, v) in self.theme.iter().enumerate() {
255 if !processed.contains(&i) {
256 self.scales.process_color(*v);
257 self.tokens.update_schema(i, self.scales.scale[i]);
258 if i < self.theme.len() {
259 for (j, w) in self.theme[i + 1..].iter().enumerate() {
260 if w == v {
261 self.tokens
262 .update_schema(j + i + 1, self.scales.scale[j + i + 1]);
263 processed.push(j + i + 1);
264 }
265 }
266 }
267 }
268 }
269 }
270
271 fn match_egui_visuals(&self, ui: &mut Ui) {
272 match self.apply_to {
273 ApplyTo::Global => self.tokens.set_ctx_visuals(ui.ctx()),
274 ApplyTo::Local => self.tokens.set_ui_visuals(ui),
275 ApplyTo::ExtraScale => {}
276 }
277 }
278
279 fn update_color(&mut self, ui: &mut Ui, i: usize) {
280 self.scales.process_color(self.theme[i]);
281 self.tokens.update_schema(i, self.scales.scale[i]);
282 self.tokens.color_on_accent();
283 if self.animated {
284 self.animator.start(ui.ctx());
285 } else {
286 self.match_egui_visuals(ui);
287 }
288 }
289
290 fn update_colors(&mut self, ctx: Option<&Context>, ui: Option<&mut Ui>) {
291 if self.animated {
292 self.process_theme();
293 self.tokens.color_on_accent();
294 if let Some(ctx) = ctx {
295 self.animator.start(ctx);
296 } else if let Some(ui) = ui {
297 self.animator.start(ui.ctx());
298 }
299 } else {
300 self.process_theme();
301 self.tokens.color_on_accent();
302 if let Some(ctx) = ctx {
303 if self.apply_to != ApplyTo::ExtraScale {
304 self.tokens.set_ctx_visuals(ctx);
305 }
306 } else if let Some(ui) = ui {
307 self.tokens.set_ui_visuals(ui);
308 }
309 }
310 }
311
312 pub fn light_dark_toggle_button(&mut self, ui: &mut Ui, size: f32) {
315 #![allow(clippy::collapsible_else_if)]
316 if self.dark_mode() {
317 if ui
318 .add(egui::Button::new(egui::RichText::new("☀").size(size)).frame(false))
319 .on_hover_text("Switch to light mode")
320 .clicked()
321 {
322 self.set_colorix_mode(false);
323 self.set_ui_mode(ui, false);
324 self.match_and_update_colors(ui);
325 }
326 } else {
327 if ui
328 .add(egui::Button::new(egui::RichText::new("🌙").size(size)).frame(false))
329 .on_hover_text("Switch to dark mode")
330 .clicked()
331 {
332 self.set_colorix_mode(true);
333 self.set_ui_mode(ui, true);
334 self.match_and_update_colors(ui);
335 }
336 }
337 }
338
339 pub fn themes_dropdown(
354 &mut self,
355 ui: &mut Ui,
356 custom_themes: Option<(Vec<&str>, Vec<Theme>)>,
357 custom_only: bool,
358 ) {
359 let combi_themes: Vec<Theme>;
360 let combi_names: Vec<&str>;
361
362 if let Some(custom) = custom_themes {
363 let (names, themes) = custom;
364 if custom_only {
365 combi_themes = themes;
366 combi_names = names;
367 } else {
368 combi_themes = THEMES.iter().copied().chain(themes).collect();
369 combi_names = THEME_NAMES.iter().copied().chain(names).collect();
370 }
371 } else {
372 combi_names = THEME_NAMES.to_vec();
373 combi_themes = THEMES.to_vec();
374 }
375 egui::ComboBox::from_id_salt(ui.id())
376 .selected_text(combi_names[self.theme_index])
377 .show_ui(ui, |ui| {
378 for i in 0..combi_themes.len() {
379 if ui
380 .selectable_value(&mut self.theme, combi_themes[i], combi_names[i])
381 .clicked()
382 {
383 self.theme_index = i;
384 self.match_and_update_colors(ui);
385 }
386 }
387 });
388 }
389 pub fn ui_combo_12(&mut self, ui: &mut Ui, copy: bool) {
391 let dropdown_colors: [ThemeColor; 23] = [
392 ThemeColor::Gray,
393 ThemeColor::EguiBlue,
394 ThemeColor::Tomato,
395 ThemeColor::Red,
396 ThemeColor::Ruby,
397 ThemeColor::Crimson,
398 ThemeColor::Pink,
399 ThemeColor::Plum,
400 ThemeColor::Purple,
401 ThemeColor::Violet,
402 ThemeColor::Iris,
403 ThemeColor::Indigo,
404 ThemeColor::Blue,
405 ThemeColor::Cyan,
406 ThemeColor::Teal,
407 ThemeColor::Jade,
408 ThemeColor::Green,
409 ThemeColor::Grass,
410 ThemeColor::Brown,
411 ThemeColor::Bronze,
412 ThemeColor::Gold,
413 ThemeColor::Orange,
414 ThemeColor::Custom(self.scales.custom()),
415 ];
416 ui.vertical(|ui| {
417 for (i, label) in LABELS.iter().enumerate() {
418 ui.horizontal(|ui| {
419 let color_edit_size = egui::vec2(40.0, 18.0);
420 if let Some(ThemeColor::Custom(rgb)) = self.theme.get_mut(i) {
421 let re = ui.color_edit_button_srgb(rgb);
422 if re.changed() {
423 self.update_color(ui, i);
424 }
425 } else {
426 ui.add_space(color_edit_size.x + ui.style().spacing.item_spacing.x);
429 }
430 let color = if self.animated {
431 self.animator.animated_tokens.get_token(i)
432 } else {
433 self.tokens.get_token(i)
434 };
435 egui::widgets::color_picker::show_color(ui, color, color_edit_size);
436 egui::ComboBox::from_label(*label)
437 .selected_text(self.theme[i].label())
438 .show_ui(ui, |ui| {
439 for preset in dropdown_colors {
440 if ui
441 .selectable_value(&mut self.theme[i], preset, preset.label())
442 .clicked()
443 {
444 self.update_color(ui, i);
445 }
446 }
447 });
448 });
449 }
450 });
451 if copy {
452 ui.add_space(10.);
453 if ui.button("Copy theme to clipboard").clicked() {
454 ui.ctx().copy_text(format!("{:#?}", self.theme));
455 }
456 }
457 }
458
459 pub fn custom_picker(&mut self, ui: &mut Ui) {
462 if egui::color_picker::color_edit_button_hsva(
463 ui,
464 &mut self.scales.custom,
465 egui::color_picker::Alpha::Opaque,
466 )
467 .changed()
468 {
469 self.scales.clamp_custom();
470 }
471 }
472
473 pub fn draw_background(&mut self, ctx: &Context, accent: bool) {
476 let (ui_element, background) = if self.animated {
477 (
478 self.animator.animated_tokens.ui_element_background,
479 self.animator.animated_tokens.app_background,
480 )
481 } else {
482 (
483 self.tokens.ui_element_background,
484 self.tokens.app_background,
485 )
486 };
487 let bg = if accent {
488 self.animator.animated_tokens.active_ui_element_background
489 } else {
490 ui_element
491 };
492 let rect = egui::Context::available_rect(ctx);
493 let layer_id = egui::LayerId::background();
494 let painter = egui::Painter::new(ctx.clone(), layer_id, rect);
495 let mut mesh = egui::Mesh::default();
496 mesh.colored_vertex(rect.left_top(), background);
497 mesh.colored_vertex(rect.right_top(), background);
498 mesh.colored_vertex(rect.left_bottom(), bg);
499 mesh.colored_vertex(rect.right_bottom(), bg);
500 mesh.add_triangle(0, 1, 2);
501 mesh.add_triangle(1, 2, 3);
502 painter.add(egui::Shape::Mesh(Arc::new(mesh)));
503 }
504 #[must_use]
506 pub const fn theme(&self) -> &Theme {
507 &self.theme
508 }
509}