From c0c69a4967199b5256794bc04ab169b3c76bec2e Mon Sep 17 00:00:00 2001 From: playfairs Date: Thu, 23 Apr 2026 21:28:20 -0500 Subject: [PATCH] ok --- Cargo.lock | 1 + crates/command_palette/src/lib.rs | 6 +- crates/editor/src/main.rs | 113 ++++---- crates/search/src/lib.rs | 45 ++-- crates/settings/src/lib.rs | 129 ++++++--- crates/theme/Cargo.toml | 1 + crates/theme/src/cherry_blossom.rs | 155 +++++++++++ crates/theme/src/lib.rs | 407 ++++++++++++++++++++++++----- crates/theme/src/rose_pine.rs | 191 ++++++++++++++ 9 files changed, 877 insertions(+), 171 deletions(-) create mode 100644 crates/theme/src/cherry_blossom.rs create mode 100644 crates/theme/src/rose_pine.rs diff --git a/Cargo.lock b/Cargo.lock index dc86f53..75271fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3487,6 +3487,7 @@ name = "theme" version = "0.1.0" dependencies = [ "egui", + "serde", ] [[package]] diff --git a/crates/command_palette/src/lib.rs b/crates/command_palette/src/lib.rs index 5376699..0370d61 100644 --- a/crates/command_palette/src/lib.rs +++ b/crates/command_palette/src/lib.rs @@ -33,7 +33,7 @@ impl CommandPalette { ui.label( egui::RichText::new(">") .size(20.0) - .color(CherryBlossomTheme::ACCENT_PINK), + .color(CherryBlossomTheme::ACCENT_PINK()), ); let response = ui.text_edit_singleline(&mut self.query); @@ -71,7 +71,7 @@ impl CommandPalette { ui.label( egui::RichText::new(name) .size(14.0) - .color(CherryBlossomTheme::TEXT_PRIMARY), + .color(CherryBlossomTheme::TEXT_PRIMARY()), ); ui.with_layout( @@ -79,7 +79,7 @@ impl CommandPalette { |ui| { ui.label( egui::RichText::new(desc).size(12.0).color( - CherryBlossomTheme::TEXT_MUTED, + CherryBlossomTheme::TEXT_MUTED(), ), ); }, diff --git a/crates/editor/src/main.rs b/crates/editor/src/main.rs index c7d3b73..06e4060 100644 --- a/crates/editor/src/main.rs +++ b/crates/editor/src/main.rs @@ -483,7 +483,7 @@ impl AsterIDE { .show(ctx, |ui| { ui.horizontal(|ui| { ui.style_mut().visuals.widgets.inactive.weak_bg_fill = - CherryBlossomTheme::BG_DARK; + CherryBlossomTheme::BG_DARK(); egui::MenuBar::new().ui(ui, |ui| { ui.menu_button("File", |ui| { @@ -596,7 +596,14 @@ impl AsterIDE { let button_size = egui::vec2(40.0, 40.0); - let explorer_active = self.active_sidebar_tab == SidebarTab::Explorer; + let active_tab_type = self.tabs.active_tab() + .map(|t| t.tab_type) + .unwrap_or(TabType::File); + + // Explorer is only active when sidebar tab is Explorer AND current tab is a file + let explorer_active = self.active_sidebar_tab == SidebarTab::Explorer + && active_tab_type == TabType::File; + if self.icon_button(ui, "📁", "Explorer", explorer_active, button_size) { self.toggle_sidebar(SidebarTab::Explorer); } @@ -642,17 +649,17 @@ impl AsterIDE { let _visuals = ui.style().interact(&response); let bg_color = if active { - CherryBlossomTheme::BG_LIGHTER + CherryBlossomTheme::BG_LIGHTER() } else if response.hovered() { - CherryBlossomTheme::BG_LIGHT + CherryBlossomTheme::BG_LIGHT() } else { - CherryBlossomTheme::BG_DARK + CherryBlossomTheme::BG_DARK() }; let fg_color = if active { - CherryBlossomTheme::ACCENT_PINK + CherryBlossomTheme::ACCENT_PINK() } else { - CherryBlossomTheme::TEXT_SECONDARY + CherryBlossomTheme::TEXT_SECONDARY() }; ui.painter().rect_filled(rect, 4.0, bg_color); @@ -666,7 +673,7 @@ impl AsterIDE { let text_pos = rect.center() - galley.size() / 2.0; ui.painter() - .galley(text_pos, galley, CherryBlossomTheme::TEXT_PRIMARY); + .galley(text_pos, galley, CherryBlossomTheme::TEXT_PRIMARY()); response.clicked() } @@ -764,11 +771,11 @@ impl AsterIDE { ); let bg_color = if is_active { - CherryBlossomTheme::BG_MID + CherryBlossomTheme::BG_MID() } else if response.hovered() { - CherryBlossomTheme::BG_LIGHT + CherryBlossomTheme::BG_LIGHT() } else { - CherryBlossomTheme::BG_DARK + CherryBlossomTheme::BG_DARK() }; let corner_radius = 6.0; @@ -779,7 +786,7 @@ impl AsterIDE { rect.left_top() + egui::vec2(4.0, 6.0), egui::vec2(3.0, button_height - 12.0), ); - ui.painter().rect_filled(indicator_rect, 1.5, CherryBlossomTheme::ACCENT_PINK); + ui.painter().rect_filled(indicator_rect, 1.5, CherryBlossomTheme::ACCENT_PINK()); } let modified_dot_x = if is_active { 18.0 } else { 14.0 }; @@ -788,14 +795,14 @@ impl AsterIDE { rect.left_center() + egui::vec2(modified_dot_x, 0.0), egui::vec2(6.0, 6.0), ); - ui.painter().circle_filled(dot_rect.center(), 3.0, CherryBlossomTheme::ACCENT_HOT); + ui.painter().circle_filled(dot_rect.center(), 3.0, CherryBlossomTheme::ACCENT_HOT()); } let text_offset = if tab.is_modified { modified_dot_x + 12.0 } else { modified_dot_x }; let text_color = if is_active { - CherryBlossomTheme::TEXT_PRIMARY + CherryBlossomTheme::TEXT_PRIMARY() } else { - CherryBlossomTheme::TEXT_SECONDARY + CherryBlossomTheme::TEXT_SECONDARY() }; ui.painter().text( @@ -907,8 +914,8 @@ impl AsterIDE { } response.context_menu(|ui| { - ui.style_mut().visuals.widgets.hovered.weak_bg_fill = theme::CherryBlossomTheme::BG_LIGHT; - ui.style_mut().visuals.widgets.hovered.bg_fill = theme::CherryBlossomTheme::BG_LIGHT; + ui.style_mut().visuals.widgets.hovered.weak_bg_fill = CherryBlossomTheme::BG_LIGHT(); + ui.style_mut().visuals.widgets.hovered.bg_fill = CherryBlossomTheme::BG_LIGHT(); if !is_dir { if ui.button("Open").clicked() { @@ -978,7 +985,7 @@ impl AsterIDE { self.delete_path(path.clone()); ui.close(); } - }); + }); } if is_expanded && is_dir { @@ -1053,9 +1060,9 @@ impl AsterIDE { let is_modified = tab.is_modified; let bg_color = if is_active { - CherryBlossomTheme::BG_MID + CherryBlossomTheme::BG_MID() } else { - CherryBlossomTheme::BG_DARK + CherryBlossomTheme::BG_DARK() }; let prefix = if is_modified { "● " } else { "" }; @@ -1079,7 +1086,7 @@ impl AsterIDE { rect.left_top() + egui::vec2(radius, 0.0), rect.right_top() + egui::vec2(-radius, 0.0), ], - egui::Stroke::new(2.0, CherryBlossomTheme::ACCENT_PINK), + egui::Stroke::new(2.0, CherryBlossomTheme::ACCENT_PINK()), ); } @@ -1087,9 +1094,9 @@ impl AsterIDE { label_text.clone(), egui::FontId::new(12.0, egui::FontFamily::Proportional), if is_active { - CherryBlossomTheme::TEXT_PRIMARY + CherryBlossomTheme::TEXT_PRIMARY() } else { - CherryBlossomTheme::TEXT_SECONDARY + CherryBlossomTheme::TEXT_SECONDARY() }, 100.0, ); @@ -1097,7 +1104,7 @@ impl AsterIDE { let text_pos = rect.left_center() + egui::vec2(10.0, -galley.size().y / 2.0); ui.painter() - .galley(text_pos, galley, CherryBlossomTheme::TEXT_PRIMARY); + .galley(text_pos, galley, CherryBlossomTheme::TEXT_PRIMARY()); let close_rect = egui::Rect::from_min_size( rect.right_top() - egui::vec2(25.0, 0.0), @@ -1115,7 +1122,7 @@ impl AsterIDE { egui::Align2::CENTER_CENTER, "×", egui::FontId::new(16.0, egui::FontFamily::Proportional), - CherryBlossomTheme::TEXT_PRIMARY, + CherryBlossomTheme::TEXT_PRIMARY(), ); } @@ -1155,9 +1162,9 @@ impl AsterIDE { ui.allocate_exact_size(button_size, egui::Sense::click()); let bg_color = if response.hovered() { - CherryBlossomTheme::BG_LIGHT + CherryBlossomTheme::BG_LIGHT() } else { - CherryBlossomTheme::BG_DARK + CherryBlossomTheme::BG_DARK() }; ui.painter().rect_filled(rect, 4.0, bg_color); @@ -1166,7 +1173,7 @@ impl AsterIDE { egui::Align2::CENTER_CENTER, "+", egui::FontId::new(16.0, egui::FontFamily::Proportional), - CherryBlossomTheme::TEXT_PRIMARY, + CherryBlossomTheme::TEXT_PRIMARY(), ); if response.clicked() { @@ -1180,7 +1187,7 @@ impl AsterIDE { fn show_welcome_screen(&mut self, ctx: &egui::Context) { egui::CentralPanel::default() - .frame(egui::Frame::central_panel(&ctx.global_style()).fill(CherryBlossomTheme::BG_DARKEST)) + .frame(egui::Frame::central_panel(&ctx.global_style()).fill(CherryBlossomTheme::BG_DARKEST())) .show(ctx, |ui| { let recent_files_data: Vec<(std::path::PathBuf, String)> = self .get_relevant_recent_files() @@ -1217,13 +1224,13 @@ impl AsterIDE { ui.heading( egui::RichText::new("AsterIDE 🌸") .size(48.0) - .color(CherryBlossomTheme::ACCENT_PINK), + .color(CherryBlossomTheme::ACCENT_PINK()), ); ui.add_space(10.0); ui.label( egui::RichText::new("A Simple Text Editor written in Rust.") .size(16.0) - .color(CherryBlossomTheme::TEXT_SECONDARY), + .color(CherryBlossomTheme::TEXT_SECONDARY()), ); }); @@ -1243,12 +1250,12 @@ impl AsterIDE { egui::Layout::top_down(egui::Align::Center), |ui| { egui::Frame::group(&ui.style()) - .fill(CherryBlossomTheme::BG_DARK) + .fill(CherryBlossomTheme::BG_DARK()) .inner_margin(20.0) - .stroke(egui::Stroke::new(1.0, CherryBlossomTheme::BORDER_PINK)) + .stroke(egui::Stroke::new(1.0, CherryBlossomTheme::BORDER_PINK())) .show(ui, |ui| { let button_size = egui::vec2(200.0, 40.0); - let button_stroke = egui::Stroke::new(1.0, CherryBlossomTheme::BORDER_PINK); + let button_stroke = egui::Stroke::new(1.0, CherryBlossomTheme::BORDER_PINK()); if ui .add_sized(button_size, egui::Button::new("📄 Open File").stroke(button_stroke)) .clicked() @@ -1290,13 +1297,13 @@ impl AsterIDE { egui::Layout::top_down(egui::Align::LEFT), |ui| { egui::Frame::group(&ui.style()) - .fill(CherryBlossomTheme::BG_DARK) + .fill(CherryBlossomTheme::BG_DARK()) .inner_margin(16.0) - .stroke(egui::Stroke::new(1.0, CherryBlossomTheme::BORDER_PINK)) + .stroke(egui::Stroke::new(1.0, CherryBlossomTheme::BORDER_PINK())) .show(ui, |ui| { ui.set_width(right_width - 32.0); - let recent_button_stroke = egui::Stroke::new(1.0, CherryBlossomTheme::BORDER_PINK); + let recent_button_stroke = egui::Stroke::new(1.0, CherryBlossomTheme::BORDER_PINK()); if has_recent_files { let title = if has_project_folder { @@ -1313,7 +1320,7 @@ impl AsterIDE { ui.label( egui::RichText::new(title) .size(16.0) - .color(CherryBlossomTheme::TEXT_PRIMARY), + .color(CherryBlossomTheme::TEXT_PRIMARY()), ); ui.add_space(10.0); @@ -1323,10 +1330,10 @@ impl AsterIDE { let response = ui.add( egui::Button::new( egui::RichText::new(format!("📄 {}", file_path_str)) - .color(CherryBlossomTheme::TEXT_PRIMARY) + .color(CherryBlossomTheme::TEXT_PRIMARY()) .size(12.0), ) - .fill(CherryBlossomTheme::BG_MID) + .fill(CherryBlossomTheme::BG_MID()) .stroke(recent_button_stroke) .min_size(egui::vec2(right_width - 50.0, 30.0)), ); @@ -1358,7 +1365,7 @@ impl AsterIDE { ui.label( egui::RichText::new("Recent Projects") .size(16.0) - .color(CherryBlossomTheme::TEXT_PRIMARY), + .color(CherryBlossomTheme::TEXT_PRIMARY()), ); ui.add_space(10.0); @@ -1368,10 +1375,10 @@ impl AsterIDE { let response = ui.add( egui::Button::new( egui::RichText::new(format!("📁 {}", project_path_str)) - .color(CherryBlossomTheme::TEXT_PRIMARY) + .color(CherryBlossomTheme::TEXT_PRIMARY()) .size(12.0), ) - .fill(CherryBlossomTheme::BG_MID) + .fill(CherryBlossomTheme::BG_MID()) .stroke(recent_button_stroke) .min_size(egui::vec2(right_width - 50.0, 30.0)), ); @@ -1411,7 +1418,7 @@ impl AsterIDE { if active_tab_type == TabType::Settings { egui::CentralPanel::default() .frame( - egui::Frame::central_panel(&ctx.global_style()).fill(CherryBlossomTheme::BG_DARKEST), + egui::Frame::central_panel(&ctx.global_style()).fill(CherryBlossomTheme::BG_DARKEST()), ) .show(ctx, |ui| { ui.set_height(ui.available_height()); @@ -1440,7 +1447,7 @@ impl AsterIDE { if active_tab_type == TabType::SearchResults { egui::CentralPanel::default() .frame( - egui::Frame::central_panel(&ctx.global_style()).fill(CherryBlossomTheme::BG_DARKEST), + egui::Frame::central_panel(&ctx.global_style()).fill(CherryBlossomTheme::BG_DARKEST()), ) .show(ctx, |ui| { let mut state: search::SearchState = ui.ctx().data_mut(|d| { @@ -1473,7 +1480,7 @@ impl AsterIDE { let _line_number_width = if _show_line_numbers { 50.0 } else { 0.0 }; egui::CentralPanel::default() - .frame(egui::Frame::central_panel(&ctx.global_style()).fill(CherryBlossomTheme::BG_DARKEST)) + .frame(egui::Frame::central_panel(&ctx.global_style()).fill(CherryBlossomTheme::BG_DARKEST())) .show(ctx, |ui| { let mut text_changed = false; let mut new_text = content.clone(); @@ -1519,7 +1526,7 @@ impl AsterIDE { ui.label( egui::RichText::new(&self.status_message) .size(11.0) - .color(CherryBlossomTheme::TEXT_SECONDARY), + .color(CherryBlossomTheme::TEXT_SECONDARY()), ); ui.add_space(ui.available_width() - 250.0); @@ -1528,7 +1535,7 @@ impl AsterIDE { ui.label( egui::RichText::new(format!("{} Ln, Col {}", 1, 1)) .size(11.0) - .color(CherryBlossomTheme::TEXT_MUTED), + .color(CherryBlossomTheme::TEXT_MUTED()), ); ui.add_space(15.0); @@ -1541,7 +1548,7 @@ impl AsterIDE { ui.label( egui::RichText::new(indent_text) .size(11.0) - .color(CherryBlossomTheme::TEXT_MUTED), + .color(CherryBlossomTheme::TEXT_MUTED()), ); ui.add_space(15.0); @@ -1549,7 +1556,7 @@ impl AsterIDE { ui.label( egui::RichText::new("UTF-8") .size(11.0) - .color(CherryBlossomTheme::TEXT_MUTED), + .color(CherryBlossomTheme::TEXT_MUTED()), ); ui.add_space(15.0); @@ -1558,7 +1565,7 @@ impl AsterIDE { ui.label( egui::RichText::new("● Modified") .size(11.0) - .color(CherryBlossomTheme::ACCENT_HOT), + .color(CherryBlossomTheme::ACCENT_HOT()), ); } } @@ -1570,8 +1577,6 @@ impl AsterIDE { self.tabs.close_active_tab(); } - // basically mark a file as pending open if we try to open a file - // while settings have not been saved if !self.settings.confirm_discard_open { if let Some((path, content)) = self.settings.take_pending_file_open() { self.tabs.open_file(path.clone(), content); @@ -1589,7 +1594,7 @@ impl eframe::App for AsterIDE { ui.set_height(ui.available_height()); let ctx = ui.ctx(); - CherryBlossomTheme::apply(ctx, self.settings.corner_roundness); + theme::apply_theme_from_settings(ctx, self.settings.theme_variant, self.settings.corner_roundness); if let Some(path) = self.initial_path.take() { if path.is_file() { diff --git a/crates/search/src/lib.rs b/crates/search/src/lib.rs index 15a401f..f94d1f7 100644 --- a/crates/search/src/lib.rs +++ b/crates/search/src/lib.rs @@ -1,4 +1,3 @@ -use theme::CherryBlossomTheme; #[derive(Clone, Default, Debug)] pub struct SearchState { @@ -87,7 +86,7 @@ pub fn show_search_tab(ui: &mut egui::Ui, state: &mut SearchState, min_chars: us ui.label( egui::RichText::new("🔍") .size(16.0) - .color(CherryBlossomTheme::TEXT_MUTED), + .color(theme::CherryBlossomTheme::TEXT_MUTED()), ); ui.add_space(8.0); @@ -114,9 +113,9 @@ pub fn show_search_tab(ui: &mut egui::Ui, state: &mut SearchState, min_chars: us ui.add_space(10.0); let case_color = if state.case_sensitive { - CherryBlossomTheme::ACCENT_PINK + theme::CherryBlossomTheme::ACCENT_PINK() } else { - CherryBlossomTheme::TEXT_MUTED + theme::CherryBlossomTheme::TEXT_MUTED() }; if ui .add( @@ -129,9 +128,9 @@ pub fn show_search_tab(ui: &mut egui::Ui, state: &mut SearchState, min_chars: us } let word_color = if state.whole_word { - CherryBlossomTheme::ACCENT_PINK + theme::CherryBlossomTheme::ACCENT_PINK() } else { - CherryBlossomTheme::TEXT_MUTED + theme::CherryBlossomTheme::TEXT_MUTED() }; if ui .add( @@ -144,9 +143,9 @@ pub fn show_search_tab(ui: &mut egui::Ui, state: &mut SearchState, min_chars: us } let regex_color = if state.use_regex { - CherryBlossomTheme::ACCENT_PINK + theme::CherryBlossomTheme::ACCENT_PINK() } else { - CherryBlossomTheme::TEXT_MUTED + theme::CherryBlossomTheme::TEXT_MUTED() }; if ui .add( @@ -164,7 +163,7 @@ pub fn show_search_tab(ui: &mut egui::Ui, state: &mut SearchState, min_chars: us ui.label( egui::RichText::new("↔") .size(16.0) - .color(CherryBlossomTheme::TEXT_MUTED), + .color(theme::CherryBlossomTheme::TEXT_MUTED()), ); ui.add_space(8.0); ui.add_sized( @@ -184,10 +183,10 @@ pub fn show_search_tab(ui: &mut egui::Ui, state: &mut SearchState, min_chars: us egui::vec2(80.0, button_height), egui::Button::new( egui::RichText::new("Search") - .color(CherryBlossomTheme::BG_DARKEST) + .color(theme::CherryBlossomTheme::BG_DARKEST()) .strong(), ) - .fill(CherryBlossomTheme::ACCENT_PINK), + .fill(theme::CherryBlossomTheme::ACCENT_PINK()), ); if search_btn.clicked() { ui.ctx().data_mut(|d| { @@ -214,13 +213,13 @@ pub fn show_search_tab(ui: &mut egui::Ui, state: &mut SearchState, min_chars: us if !state.results.is_empty() { ui.label( egui::RichText::new(format!("{} matches", state.results.len())) - .color(CherryBlossomTheme::TEXT_MUTED) + .color(theme::CherryBlossomTheme::TEXT_MUTED()) .size(12.0), ); } else if !state.query.is_empty() { ui.label( egui::RichText::new("No results") - .color(CherryBlossomTheme::TEXT_MUTED) + .color(theme::CherryBlossomTheme::TEXT_MUTED()) .size(12.0), ); } @@ -236,7 +235,7 @@ pub fn show_search_tab(ui: &mut egui::Ui, state: &mut SearchState, min_chars: us ui.centered_and_justified(|ui| { ui.label( egui::RichText::new("Enter a search query to find across open files") - .color(CherryBlossomTheme::TEXT_MUTED) + .color(theme::CherryBlossomTheme::TEXT_MUTED()) .size(14.0), ); }); @@ -251,7 +250,7 @@ pub fn show_search_tab(ui: &mut egui::Ui, state: &mut SearchState, min_chars: us ui.label(egui::RichText::new("📄 ").size(12.0)); ui.label( egui::RichText::new(&result.file_path) - .color(CherryBlossomTheme::ACCENT_PINK) + .color(theme::CherryBlossomTheme::ACCENT_PINK()) .size(12.0) .strong(), ); @@ -263,9 +262,9 @@ pub fn show_search_tab(ui: &mut egui::Ui, state: &mut SearchState, min_chars: us let is_current = i == state.current_result; let bg_color = if is_current { - CherryBlossomTheme::BG_DARK + theme::CherryBlossomTheme::BG_DARK() } else { - CherryBlossomTheme::BG_DARKEST + theme::CherryBlossomTheme::BG_DARKEST() }; let response = egui::Frame::new() @@ -277,7 +276,7 @@ pub fn show_search_tab(ui: &mut egui::Ui, state: &mut SearchState, min_chars: us egui::vec2(50.0, 18.0), egui::Label::new( egui::RichText::new(format!("{}", result.line)) - .color(CherryBlossomTheme::TEXT_MUTED) + .color(theme::CherryBlossomTheme::TEXT_MUTED()) .monospace() .size(12.0), ), @@ -301,18 +300,18 @@ pub fn show_search_tab(ui: &mut egui::Ui, state: &mut SearchState, min_chars: us ui.horizontal(|ui| { ui.monospace( egui::RichText::new(before) - .color(CherryBlossomTheme::TEXT_SECONDARY) + .color(theme::CherryBlossomTheme::TEXT_SECONDARY()) .size(12.0), ); ui.monospace( egui::RichText::new(matched) - .color(CherryBlossomTheme::ACCENT_HOT) + .color(theme::CherryBlossomTheme::ACCENT_HOT()) .strong() .size(12.0), ); ui.monospace( egui::RichText::new(after) - .color(CherryBlossomTheme::TEXT_SECONDARY) + .color(theme::CherryBlossomTheme::TEXT_SECONDARY()) .size(12.0), ); }); @@ -341,12 +340,12 @@ pub fn show_search_button(ui: &mut egui::Ui) { ui.label( egui::RichText::new("🔍") .size(32.0) - .color(CherryBlossomTheme::TEXT_MUTED), + .color(theme::CherryBlossomTheme::TEXT_MUTED()), ); ui.add_space(10.0); ui.label( egui::RichText::new("Click the search icon above\nor press Ctrl+Shift+F") - .color(CherryBlossomTheme::TEXT_MUTED) + .color(theme::CherryBlossomTheme::TEXT_MUTED()) .size(11.0), ); }); diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index 1b558c4..1f48bdd 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -37,6 +37,8 @@ pub struct Settings { pub recent_files_limit: usize, pub recent_projects_limit: usize, pub corner_roundness: f32, + pub theme_family: theme::ThemeFamily, + pub theme_variant: theme::ThemeVariant, #[serde(skip)] pub selected_category: SettingsCategory, #[serde(skip)] @@ -81,6 +83,8 @@ impl Default for Settings { recent_files_limit: 5, recent_projects_limit: 5, corner_roundness: 6.0, + theme_family: theme::ThemeFamily::CherryBlossom, + theme_variant: theme::ThemeVariant::CherryBlossomDark, selected_category: SettingsCategory::default(), search_query: String::new(), edit_as_json_clicked: false, @@ -175,6 +179,8 @@ impl Settings { || self.recent_files_limit != saved.recent_files_limit || self.recent_projects_limit != saved.recent_projects_limit || self.corner_roundness != saved.corner_roundness + || self.theme_family != saved.theme_family + || self.theme_variant != saved.theme_variant } else { false } @@ -208,6 +214,8 @@ impl Settings { self.recent_files_limit = saved.recent_files_limit; self.recent_projects_limit = saved.recent_projects_limit; self.corner_roundness = saved.corner_roundness; + self.theme_family = saved.theme_family; + self.theme_variant = saved.theme_variant; } } @@ -255,9 +263,9 @@ impl Settings { }); let modal_frame = egui::Frame::new() - .fill(theme::CherryBlossomTheme::BG_DARKEST) + .fill(theme::CherryBlossomTheme::BG_DARKEST()) .corner_radius(12.0) - .stroke(egui::Stroke::new(1.0, theme::CherryBlossomTheme::BG_LIGHT)) + .stroke(egui::Stroke::new(1.0, theme::CherryBlossomTheme::BG_LIGHT())) .inner_margin(egui::Margin::symmetric(32, 28)) .shadow(egui::epaint::Shadow { offset: [0, 8], @@ -280,18 +288,18 @@ impl Settings { egui::RichText::new("Unsaved Changes") .size(18.0) .strong() - .color(theme::CherryBlossomTheme::TEXT_PRIMARY), + .color(theme::CherryBlossomTheme::TEXT_PRIMARY()), ); ui.add_space(12.0); ui.label( egui::RichText::new("You have unsaved settings changes.") .size(14.0) - .color(theme::CherryBlossomTheme::TEXT_SECONDARY), + .color(theme::CherryBlossomTheme::TEXT_SECONDARY()), ); ui.label( egui::RichText::new("Discard them?") .size(14.0) - .color(theme::CherryBlossomTheme::TEXT_SECONDARY), + .color(theme::CherryBlossomTheme::TEXT_SECONDARY()), ); ui.add_space(24.0); @@ -304,10 +312,10 @@ impl Settings { egui::RichText::new("Discard") .size(14.0) .strong() - .color(theme::CherryBlossomTheme::BG_DARKEST), + .color(theme::CherryBlossomTheme::BG_DARKEST()), ) .corner_radius(8.0) - .fill(theme::CherryBlossomTheme::ACCENT_PINK), + .fill(theme::CherryBlossomTheme::ACCENT_PINK()), ); if discard_btn.clicked() { self.discard_changes(); @@ -325,10 +333,10 @@ impl Settings { egui::Button::new( egui::RichText::new("Cancel") .size(14.0) - .color(theme::CherryBlossomTheme::TEXT_PRIMARY), + .color(theme::CherryBlossomTheme::TEXT_PRIMARY()), ) .corner_radius(8.0) - .fill(theme::CherryBlossomTheme::BG_MID), + .fill(theme::CherryBlossomTheme::BG_MID()), ); if cancel_btn.clicked() { self.confirm_discard_open = false; @@ -399,7 +407,7 @@ impl Settings { egui::RichText::new(self.selected_category.name()) .size(18.0) .strong() - .color(CherryBlossomTheme::TEXT_PRIMARY), + .color(theme::CherryBlossomTheme::TEXT_PRIMARY()), ); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { @@ -413,10 +421,10 @@ impl Settings { egui::RichText::new("Apply Changes") .size(13.0) .strong() - .color(theme::CherryBlossomTheme::BG_DARKEST), + .color(theme::CherryBlossomTheme::BG_DARKEST()), ) .corner_radius(btn_rounding) - .fill(theme::CherryBlossomTheme::ACCENT_PINK), + .fill(theme::CherryBlossomTheme::ACCENT_PINK()), ); if apply_btn.clicked() { self.apply_changes_clicked = true; @@ -429,10 +437,10 @@ impl Settings { egui::Button::new( egui::RichText::new("Edit as JSON") .size(13.0) - .color(theme::CherryBlossomTheme::TEXT_PRIMARY), + .color(theme::CherryBlossomTheme::TEXT_PRIMARY()), ) .corner_radius(btn_rounding) - .fill(theme::CherryBlossomTheme::BG_MID), + .fill(theme::CherryBlossomTheme::BG_MID()), ); if json_btn.clicked() { self.edit_as_json_clicked = true; @@ -445,10 +453,10 @@ impl Settings { egui::Button::new( egui::RichText::new("Reset Settings") .size(13.0) - .color(theme::CherryBlossomTheme::TEXT_PRIMARY), + .color(theme::CherryBlossomTheme::TEXT_PRIMARY()), ) .corner_radius(btn_rounding) - .fill(theme::CherryBlossomTheme::BG_MID), + .fill(theme::CherryBlossomTheme::BG_MID()), ); if reset_btn.clicked() { *self = Self::default(); @@ -466,7 +474,7 @@ impl Settings { ui.label( egui::RichText::new(format!("{} / {} Settings", matches, total_count)) .size(12.0) - .color(CherryBlossomTheme::TEXT_MUTED), + .color(theme::CherryBlossomTheme::TEXT_MUTED()), ); ui.add_space(16.0); @@ -510,11 +518,11 @@ impl Settings { ); let bg_color = if is_selected { - CherryBlossomTheme::BG_MID + theme::CherryBlossomTheme::BG_MID() } else if response.hovered() { - CherryBlossomTheme::BG_LIGHT + theme::CherryBlossomTheme::BG_LIGHT() } else { - CherryBlossomTheme::BG_DARK + theme::CherryBlossomTheme::BG_DARK() }; ui.painter().rect_filled(rect, corner_radius, bg_color); @@ -524,14 +532,14 @@ impl Settings { rect.left_top() + egui::vec2(4.0, 8.0), egui::vec2(3.0, item_height - 16.0), ); - ui.painter().rect_filled(indicator_rect, 1.5, CherryBlossomTheme::ACCENT_PINK); + ui.painter().rect_filled(indicator_rect, 1.5, theme::CherryBlossomTheme::ACCENT_PINK()); } let text = format!("{}", category.name()); let text_color = if is_selected { - CherryBlossomTheme::TEXT_PRIMARY + theme::CherryBlossomTheme::TEXT_PRIMARY() } else { - CherryBlossomTheme::TEXT_SECONDARY + theme::CherryBlossomTheme::TEXT_SECONDARY() }; let text_x = if is_selected { 16.0 } else { 12.0 }; @@ -790,6 +798,63 @@ impl Settings { fn show_appearance_settings(&mut self, ui: &mut egui::Ui, has_search: bool, query: &str) { let query = query.to_lowercase(); + if !has_search || self.matches_search(&query, &["theme", "color scheme"]) { + self.setting_card(ui, "Theme", |ui, settings| { + settings.cozy_row_filtered( + ui, + has_search, + &query, + "Theme Family", + "Select the theme family", + |ui, settings| { + egui::ComboBox::from_id_salt("settings_theme_family") + .selected_text(settings.theme_family.name()) + .width(140.0) + .show_ui(ui, |ui| { + for family in theme::ThemeManager::all_families() { + if ui + .selectable_label( + settings.theme_family == *family, + family.name(), + ) + .clicked() + { + settings.theme_family = *family; + settings.theme_variant = family.default_variant(); + } + } + }); + }, + ); + settings.cozy_row_filtered( + ui, + has_search, + &query, + "Theme Variant", + "Select the theme variant", + |ui, settings| { + egui::ComboBox::from_id_salt("settings_theme_variant") + .selected_text(settings.theme_variant.name()) + .width(140.0) + .show_ui(ui, |ui| { + for &variant in settings.theme_family.variants() { + if ui + .selectable_label( + settings.theme_variant == variant, + variant.name(), + ) + .clicked() + { + settings.theme_variant = variant; + } + } + }); + }, + ); + }); + ui.add_space(12.0); + } + self.setting_card(ui, "UI Elements", |ui, settings| { settings.cozy_row_filtered( ui, @@ -895,12 +960,12 @@ impl Settings { ui.label( egui::RichText::new("Ignored patterns") .size(13.0) - .color(CherryBlossomTheme::TEXT_PRIMARY), + .color(theme::CherryBlossomTheme::TEXT_PRIMARY()), ); ui.label( egui::RichText::new("Comma-separated, use * for wildcards") .size(11.0) - .color(CherryBlossomTheme::TEXT_MUTED), + .color(theme::CherryBlossomTheme::TEXT_MUTED()), ); ui.add_space(4.0); ui.add( @@ -911,7 +976,7 @@ impl Settings { ui.label( egui::RichText::new("Examples: .git, node_modules, *venv") .size(11.0) - .color(CherryBlossomTheme::TEXT_MUTED), + .color(theme::CherryBlossomTheme::TEXT_MUTED()), ); ui.add_space(16.0); } @@ -947,9 +1012,9 @@ impl Settings { let card_margin = 16.0; egui::Frame::group(ui.style()) - .fill(CherryBlossomTheme::BG_DARK) + .fill(theme::CherryBlossomTheme::BG_DARK()) .corner_radius(self.corner_roundness) - .stroke(egui::Stroke::new(1.0, CherryBlossomTheme::BG_LIGHT)) + .stroke(egui::Stroke::new(1.0, theme::CherryBlossomTheme::BG_LIGHT())) .inner_margin(egui::Margin::same(card_margin as i8)) .show(ui, |ui| { ui.set_width(ui.available_width()); @@ -958,7 +1023,7 @@ impl Settings { egui::RichText::new(title) .size(14.0) .strong() - .color(CherryBlossomTheme::TEXT_PRIMARY), + .color(theme::CherryBlossomTheme::TEXT_PRIMARY()), ); ui.add_space(12.0); @@ -968,7 +1033,7 @@ impl Settings { ui.cursor().left_center(), ui.cursor().left_center() + egui::vec2(ui.available_width(), 0.0), ], - egui::Stroke::new(1.0, CherryBlossomTheme::BG_LIGHT), + egui::Stroke::new(1.0, theme::CherryBlossomTheme::BG_LIGHT()), ); ui.add_space(12.0); @@ -994,12 +1059,12 @@ impl Settings { ui.label( egui::RichText::new(title) .size(13.0) - .color(CherryBlossomTheme::TEXT_PRIMARY), + .color(theme::CherryBlossomTheme::TEXT_PRIMARY()), ); ui.label( egui::RichText::new(description) .size(11.0) - .color(CherryBlossomTheme::TEXT_MUTED), + .color(theme::CherryBlossomTheme::TEXT_MUTED()), ); }); diff --git a/crates/theme/Cargo.toml b/crates/theme/Cargo.toml index fe56117..e2c0d71 100644 --- a/crates/theme/Cargo.toml +++ b/crates/theme/Cargo.toml @@ -5,3 +5,4 @@ edition = "2024" [dependencies] egui = "0.34.1" +serde = { version = "1.0", features = ["derive"] } diff --git a/crates/theme/src/cherry_blossom.rs b/crates/theme/src/cherry_blossom.rs new file mode 100644 index 0000000..2a878aa --- /dev/null +++ b/crates/theme/src/cherry_blossom.rs @@ -0,0 +1,155 @@ +use egui::{Color32, FontId, TextStyle, Visuals}; + +pub struct CherryBlossomDark; + +impl CherryBlossomDark { + pub const PINK_50: Color32 = Color32::from_rgb(255, 250, 252); + pub const PINK_100: Color32 = Color32::from_rgb(255, 235, 245); + pub const PINK_200: Color32 = Color32::from_rgb(255, 210, 230); + pub const PINK_300: Color32 = Color32::from_rgb(255, 180, 210); + pub const PINK_400: Color32 = Color32::from_rgb(255, 145, 185); + pub const PINK_500: Color32 = Color32::from_rgb(255, 110, 155); + pub const PINK_600: Color32 = Color32::from_rgb(230, 80, 130); + pub const PINK_700: Color32 = Color32::from_rgb(195, 55, 105); + pub const PINK_800: Color32 = Color32::from_rgb(160, 40, 85); + pub const PINK_900: Color32 = Color32::from_rgb(130, 30, 65); + + pub const BG_DARKEST: Color32 = Color32::from_rgb(35, 20, 28); + pub const BG_DARK: Color32 = Color32::from_rgb(45, 28, 38); + pub const BG_MID: Color32 = Color32::from_rgb(55, 35, 45); + pub const BG_LIGHT: Color32 = Color32::from_rgb(70, 45, 58); + pub const BG_LIGHTER: Color32 = Color32::from_rgb(85, 55, 70); + pub const BORDER_PINK: Color32 = Color32::from_rgb(85, 55, 70); + + pub const TEXT_PRIMARY: Color32 = Color32::from_rgb(255, 235, 245); + pub const TEXT_SECONDARY: Color32 = Color32::from_rgb(200, 160, 180); + pub const TEXT_MUTED: Color32 = Color32::from_rgb(150, 110, 130); + + pub const ACCENT_PINK: Color32 = Color32::from_rgb(255, 130, 180); + pub const ACCENT_HOT: Color32 = Color32::from_rgb(255, 90, 150); + pub const ACCENT_LIGHT: Color32 = Color32::from_rgb(255, 200, 220); + + pub fn apply(ctx: &egui::Context, corner_roundness: f32) { + let mut visuals = Visuals::dark(); + let radius = egui::CornerRadius::same(corner_roundness.clamp(0.0, 255.0) as u8); + + visuals.window_fill = Self::BG_DARK; + visuals.panel_fill = Self::BG_DARK; + visuals.window_stroke = egui::Stroke::new(1.0, Self::BG_LIGHT); + visuals.window_corner_radius = radius; + visuals.menu_corner_radius = radius; + + visuals.widgets.noninteractive.corner_radius = radius; + visuals.widgets.noninteractive.bg_fill = Self::BG_MID; + visuals.widgets.inactive.corner_radius = radius; + visuals.widgets.inactive.bg_fill = Self::BG_LIGHT; + visuals.widgets.hovered.corner_radius = radius; + visuals.widgets.hovered.bg_fill = Self::BG_LIGHTER; + visuals.widgets.active.corner_radius = radius; + visuals.widgets.active.bg_fill = Self::PINK_700; + visuals.widgets.open.corner_radius = radius; + visuals.widgets.open.bg_fill = Self::PINK_600; + + visuals.selection.bg_fill = Self::PINK_600; + visuals.selection.stroke = egui::Stroke::new(1.0, Self::ACCENT_LIGHT); + + visuals.override_text_color = Some(Self::TEXT_PRIMARY); + visuals.hyperlink_color = Self::ACCENT_PINK; + + visuals.widgets.inactive.fg_stroke = egui::Stroke::new(1.0, Self::TEXT_PRIMARY); + visuals.widgets.hovered.fg_stroke = egui::Stroke::new(1.0, Self::ACCENT_LIGHT); + visuals.widgets.active.fg_stroke = egui::Stroke::new(1.0, Self::PINK_50); + + ctx.set_visuals(visuals); + Self::apply_fonts(ctx); + } + + fn apply_fonts(ctx: &egui::Context) { + let mut style = (*ctx.global_style()).clone(); + style.text_styles.insert( + TextStyle::Heading, + FontId::new(20.0, egui::FontFamily::Proportional), + ); + style.text_styles.insert( + TextStyle::Body, + FontId::new(14.0, egui::FontFamily::Proportional), + ); + style.text_styles.insert( + TextStyle::Monospace, + FontId::new(13.0, egui::FontFamily::Monospace), + ); + style.text_styles.insert( + TextStyle::Button, + FontId::new(13.0, egui::FontFamily::Proportional), + ); + style.text_styles.insert( + TextStyle::Small, + FontId::new(11.0, egui::FontFamily::Proportional), + ); + ctx.set_global_style(style); + } +} + +pub struct CherryBlossomLight; + +impl CherryBlossomLight { + pub const PINK_50: Color32 = Color32::from_rgb(255, 250, 252); + pub const PINK_100: Color32 = Color32::from_rgb(255, 240, 248); + pub const PINK_200: Color32 = Color32::from_rgb(255, 225, 240); + pub const PINK_300: Color32 = Color32::from_rgb(255, 200, 225); + pub const PINK_400: Color32 = Color32::from_rgb(255, 170, 205); + pub const PINK_500: Color32 = Color32::from_rgb(255, 140, 185); + pub const PINK_600: Color32 = Color32::from_rgb(235, 110, 160); + pub const PINK_700: Color32 = Color32::from_rgb(210, 85, 135); + pub const PINK_800: Color32 = Color32::from_rgb(180, 65, 110); + pub const PINK_900: Color32 = Color32::from_rgb(145, 50, 85); + + pub const BG_LIGHTEST: Color32 = Color32::from_rgb(255, 252, 254); + pub const BG_LIGHT: Color32 = Color32::from_rgb(255, 248, 252); + pub const BG_MID: Color32 = Color32::from_rgb(255, 240, 248); + pub const BG_DARK: Color32 = Color32::from_rgb(245, 230, 240); + pub const BG_DARKER: Color32 = Color32::from_rgb(235, 220, 230); + + pub const TEXT_PRIMARY: Color32 = Color32::from_rgb(80, 40, 60); + pub const TEXT_SECONDARY: Color32 = Color32::from_rgb(130, 80, 105); + pub const TEXT_MUTED: Color32 = Color32::from_rgb(170, 120, 145); + + pub const ACCENT_PINK: Color32 = Color32::from_rgb(220, 80, 140); + pub const ACCENT_HOT: Color32 = Color32::from_rgb(255, 90, 150); + pub const ACCENT_LIGHT: Color32 = Color32::from_rgb(255, 180, 210); + + pub fn apply(ctx: &egui::Context, corner_roundness: f32) { + let mut visuals = Visuals::light(); + let radius = egui::CornerRadius::same(corner_roundness.clamp(0.0, 255.0) as u8); + + visuals.window_fill = Self::BG_LIGHT; + visuals.panel_fill = Self::BG_LIGHT; + visuals.window_stroke = egui::Stroke::new(1.0, Self::BG_DARK); + visuals.window_corner_radius = radius; + visuals.menu_corner_radius = radius; + + visuals.widgets.noninteractive.corner_radius = radius; + visuals.widgets.noninteractive.bg_fill = Self::BG_MID; + visuals.widgets.inactive.corner_radius = radius; + visuals.widgets.inactive.bg_fill = Self::BG_DARK; + visuals.widgets.hovered.corner_radius = radius; + visuals.widgets.hovered.bg_fill = Self::BG_DARKER; + visuals.widgets.active.corner_radius = radius; + visuals.widgets.active.bg_fill = Self::PINK_400; + visuals.widgets.open.corner_radius = radius; + visuals.widgets.open.bg_fill = Self::PINK_500; + + visuals.selection.bg_fill = Self::PINK_400; + visuals.selection.stroke = egui::Stroke::new(1.0, Self::PINK_700); + + visuals.override_text_color = Some(Self::TEXT_PRIMARY); + visuals.hyperlink_color = Self::ACCENT_PINK; + + visuals.widgets.inactive.fg_stroke = egui::Stroke::new(1.0, Self::TEXT_PRIMARY); + visuals.widgets.hovered.fg_stroke = egui::Stroke::new(1.0, Self::PINK_700); + visuals.widgets.active.fg_stroke = egui::Stroke::new(1.0, Self::PINK_50); + + ctx.set_visuals(visuals); + CherryBlossomDark::apply_fonts(ctx); + } +} diff --git a/crates/theme/src/lib.rs b/crates/theme/src/lib.rs index 53aef01..ed0b7d2 100644 --- a/crates/theme/src/lib.rs +++ b/crates/theme/src/lib.rs @@ -1,97 +1,386 @@ -//! Cherry Blossom Theme for AsterIDE -// This'll be the default theme for now, -// I need to actually redo the whole editor directory to sort the editor logic -// from the themes and settings logic, and probably other stuff as well +use egui::Color32; +use serde::{Deserialize, Serialize}; +use std::sync::RwLock; -use egui::{Color32, FontId, TextStyle, Visuals}; +pub mod cherry_blossom; +pub mod rose_pine; + +static CURRENT_THEME_COLORS: RwLock = RwLock::new(ThemeColors::cherry_blossom_dark()); + +#[derive(Debug, Clone, Copy)] +pub struct ThemeColors { + pub bg_darkest: Color32, + pub bg_dark: Color32, + pub bg_mid: Color32, + pub bg_light: Color32, + pub bg_lighter: Color32, + pub border: Color32, + + pub text_primary: Color32, + pub text_secondary: Color32, + pub text_muted: Color32, + + pub accent_primary: Color32, + pub accent_hot: Color32, + pub accent_light: Color32, +} + +impl ThemeColors { + pub const fn cherry_blossom_dark() -> Self { + Self { + bg_darkest: Color32::from_rgb(35, 20, 28), + bg_dark: Color32::from_rgb(45, 28, 38), + bg_mid: Color32::from_rgb(55, 35, 45), + bg_light: Color32::from_rgb(70, 45, 58), + bg_lighter: Color32::from_rgb(85, 55, 70), + border: Color32::from_rgb(85, 55, 70), + text_primary: Color32::from_rgb(255, 235, 245), + text_secondary: Color32::from_rgb(200, 160, 180), + text_muted: Color32::from_rgb(150, 110, 130), + accent_primary: Color32::from_rgb(255, 130, 180), + accent_hot: Color32::from_rgb(255, 90, 150), + accent_light: Color32::from_rgb(255, 200, 220), + } + } + + pub const fn cherry_blossom_light() -> Self { + Self { + bg_darkest: Color32::from_rgb(245, 220, 230), + bg_dark: Color32::from_rgb(255, 248, 252), + bg_mid: Color32::from_rgb(255, 240, 248), + bg_light: Color32::from_rgb(245, 230, 240), + bg_lighter: Color32::from_rgb(235, 220, 230), + border: Color32::from_rgb(235, 220, 230), + text_primary: Color32::from_rgb(80, 40, 60), + text_secondary: Color32::from_rgb(130, 80, 105), + text_muted: Color32::from_rgb(170, 120, 145), + accent_primary: Color32::from_rgb(220, 80, 140), + accent_hot: Color32::from_rgb(255, 90, 150), + accent_light: Color32::from_rgb(255, 180, 210), + } + } + + pub const fn rose_pine() -> Self { + Self { + bg_darkest: Color32::from_rgb(31, 29, 46), // Surface #1f1d2e (base color) + bg_dark: Color32::from_rgb(38, 35, 58), // Overlay #26233a (containers) + bg_mid: Color32::from_rgb(33, 32, 46), // Highlight Low #21202e (buttons) + bg_light: Color32::from_rgb(49, 46, 73), // Highlight Low blended + bg_lighter: Color32::from_rgb(64, 61, 82), // Highlight Med #403d52 + border: Color32::from_rgb(82, 79, 103), // Highlight High #524f67 + text_primary: Color32::from_rgb(224, 222, 244), // Text #e0def4 + text_secondary: Color32::from_rgb(144, 140, 170), // Subtle #908caa + text_muted: Color32::from_rgb(110, 106, 134), // Muted #6e6a86 + accent_primary: Color32::from_rgb(196, 167, 231), // Iris #c4a7e7 (special text, buttons) + accent_hot: Color32::from_rgb(196, 167, 231), // Iris #c4a7e7 + accent_light: Color32::from_rgb(156, 207, 216), // Foam #9ccfd8 + } + } + + pub const fn rose_pine_moon() -> Self { + Self { + bg_darkest: Color32::from_rgb(35, 33, 54), // Base #232136 + bg_dark: Color32::from_rgb(42, 39, 63), // Surface #2a273f + bg_mid: Color32::from_rgb(57, 53, 82), // Overlay #393552 + bg_light: Color32::from_rgb(42, 40, 62), // Highlight Low #2a283e + bg_lighter: Color32::from_rgb(68, 65, 90), // Highlight Med #44415a + border: Color32::from_rgb(86, 82, 110), // Highlight High #56526e + text_primary: Color32::from_rgb(224, 222, 244), // Text #e0def4 + text_secondary: Color32::from_rgb(144, 140, 170), // Subtle #908caa + text_muted: Color32::from_rgb(110, 106, 134), // Muted #6e6a86 + accent_primary: Color32::from_rgb(196, 167, 231), // Iris #c4a7e7 + accent_hot: Color32::from_rgb(196, 167, 231), // Iris #c4a7e7 + accent_light: Color32::from_rgb(156, 207, 216), // Foam #9ccfd8 + } + } + + pub const fn rose_pine_dawn() -> Self { + Self { + bg_darkest: Color32::from_rgb(250, 244, 237), // Base #faf4ed + bg_dark: Color32::from_rgb(255, 250, 243), // Surface #fffaf3 + bg_mid: Color32::from_rgb(242, 233, 222), // Overlay #f2e9e1 + bg_light: Color32::from_rgb(223, 218, 211), // Highlight Low #dfdad9 + bg_lighter: Color32::from_rgb(206, 202, 195), // Highlight Med #cecacd + border: Color32::from_rgb(189, 185, 177), // Highlight High #bdb5b0 + text_primary: Color32::from_rgb(87, 82, 91), // Text #575279 + text_secondary: Color32::from_rgb(121, 112, 122), // Subtle #797593 + text_muted: Color32::from_rgb(152, 147, 165), // Muted #9893a5 + accent_primary: Color32::from_rgb(196, 167, 231), // Iris #c4a7e7 + accent_hot: Color32::from_rgb(196, 167, 231), // Iris #c4a7e7 + accent_light: Color32::from_rgb(156, 207, 216), // Foam #9ccfd8 + } + } + + pub fn for_variant(variant: ThemeVariant) -> Self { + match variant { + ThemeVariant::CherryBlossomDark => Self::cherry_blossom_dark(), + ThemeVariant::CherryBlossomLight => Self::cherry_blossom_light(), + ThemeVariant::RosePine => Self::rose_pine(), + ThemeVariant::RosePineMoon => Self::rose_pine_moon(), + ThemeVariant::RosePineDawn => Self::rose_pine_dawn(), + } + } +} + +pub fn current_theme_colors() -> ThemeColors { + CURRENT_THEME_COLORS.read().map(|c| *c).unwrap_or_else(|_| ThemeColors::cherry_blossom_dark()) +} + +pub fn set_current_theme_colors(colors: ThemeColors) { + if let Ok(mut guard) = CURRENT_THEME_COLORS.write() { + *guard = colors; + } +} + +pub fn update_current_theme(variant: ThemeVariant) { + set_current_theme_colors(ThemeColors::for_variant(variant)); +} + +pub use cherry_blossom::{CherryBlossomDark, CherryBlossomLight}; +pub use rose_pine::{RosePine, RosePineMoon, RosePineDawn}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ThemeVariant { + CherryBlossomDark, + CherryBlossomLight, + RosePine, + RosePineMoon, + RosePineDawn, +} + +impl ThemeVariant { + pub fn name(&self) -> &'static str { + match self { + ThemeVariant::CherryBlossomDark => "Dark", + ThemeVariant::CherryBlossomLight => "Light", + ThemeVariant::RosePine => "Rose Pine", + ThemeVariant::RosePineMoon => "Rose Pine Moon", + ThemeVariant::RosePineDawn => "Rose Pine Dawn", + } + } + + pub fn apply(&self, ctx: &egui::Context, corner_roundness: f32) { + match self { + ThemeVariant::CherryBlossomDark => CherryBlossomDark::apply(ctx, corner_roundness), + ThemeVariant::CherryBlossomLight => CherryBlossomLight::apply(ctx, corner_roundness), + ThemeVariant::RosePine => RosePine::apply(ctx, corner_roundness), + ThemeVariant::RosePineMoon => RosePineMoon::apply(ctx, corner_roundness), + ThemeVariant::RosePineDawn => RosePineDawn::apply(ctx, corner_roundness), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ThemeFamily { + CherryBlossom, + RosePine, +} + +impl ThemeFamily { + pub fn name(&self) -> &'static str { + match self { + ThemeFamily::CherryBlossom => "Cherry Blossom", + ThemeFamily::RosePine => "Rose Pine", + } + } + + pub fn variants(&self) -> &'static [ThemeVariant] { + match self { + ThemeFamily::CherryBlossom => &[ + ThemeVariant::CherryBlossomDark, + ThemeVariant::CherryBlossomLight, + ], + ThemeFamily::RosePine => &[ + ThemeVariant::RosePine, + ThemeVariant::RosePineMoon, + ThemeVariant::RosePineDawn, + ], + } + } + + pub fn default_variant(&self) -> ThemeVariant { + match self { + ThemeFamily::CherryBlossom => ThemeVariant::CherryBlossomDark, + ThemeFamily::RosePine => ThemeVariant::RosePine, + } + } +} + +pub struct ThemeManager { + pub current_family: ThemeFamily, + pub current_variant: ThemeVariant, + pub corner_roundness: f32, + pub show_family_dropdown: bool, + pub show_variant_dropdown: bool, +} + +impl Default for ThemeManager { + fn default() -> Self { + Self { + current_family: ThemeFamily::CherryBlossom, + current_variant: ThemeVariant::CherryBlossomDark, + corner_roundness: 8.0, + show_family_dropdown: false, + show_variant_dropdown: false, + } + } +} + +impl ThemeManager { + pub fn new() -> Self { + Self::default() + } + + pub fn apply(&self, ctx: &egui::Context) { + self.current_variant.apply(ctx, self.corner_roundness); + } + + pub fn set_family(&mut self, family: ThemeFamily) { + self.current_family = family; + self.current_variant = family.default_variant(); + } + + pub fn set_variant(&mut self, variant: ThemeVariant) { + self.current_variant = variant; + self.current_family = match variant { + ThemeVariant::CherryBlossomDark | ThemeVariant::CherryBlossomLight => { + ThemeFamily::CherryBlossom + } + ThemeVariant::RosePine | ThemeVariant::RosePineMoon | ThemeVariant::RosePineDawn => { + ThemeFamily::RosePine + } + }; + } + + pub fn ui(&mut self, ui: &mut egui::Ui) { + ui.label("Theme:"); + + egui::ComboBox::from_id_salt("theme_family") + .selected_text(self.current_family.name()) + .show_ui(ui, |ui| { + for family in [ThemeFamily::CherryBlossom, ThemeFamily::RosePine] { + if ui + .selectable_label(self.current_family == family, family.name()) + .clicked() + { + self.set_family(family); + } + } + }); + + ui.add_space(8.0); + + egui::ComboBox::from_id_salt("theme_variant") + .selected_text(self.current_variant.name()) + .show_ui(ui, |ui| { + for &variant in self.current_family.variants() { + if ui + .selectable_label(self.current_variant == variant, variant.name()) + .clicked() + { + self.current_variant = variant; + } + } + }); + + ui.add_space(8.0); + + ui.label("Corner Roundness:"); + ui.add(egui::Slider::new(&mut self.corner_roundness, 0.0..=20.0)); + + if ui.button("Apply Theme").clicked() { + self.apply(ui.ctx()); + } + } + + pub fn all_families() -> &'static [ThemeFamily] { + &[ThemeFamily::CherryBlossom, ThemeFamily::RosePine] + } +} pub struct CherryBlossomTheme; impl CherryBlossomTheme { - pub const PINK_50: Color32 = Color32::from_rgb(255, 250, 252); - pub const PINK_100: Color32 = Color32::from_rgb(255, 235, 245); - pub const PINK_200: Color32 = Color32::from_rgb(255, 210, 230); - pub const PINK_300: Color32 = Color32::from_rgb(255, 180, 210); - pub const PINK_400: Color32 = Color32::from_rgb(255, 145, 185); - pub const PINK_500: Color32 = Color32::from_rgb(255, 110, 155); - pub const PINK_600: Color32 = Color32::from_rgb(230, 80, 130); - pub const PINK_700: Color32 = Color32::from_rgb(195, 55, 105); - pub const PINK_800: Color32 = Color32::from_rgb(160, 40, 85); - pub const PINK_900: Color32 = Color32::from_rgb(130, 30, 65); - - pub const BG_DARKEST: Color32 = Color32::from_rgb(35, 20, 28); - pub const BG_DARK: Color32 = Color32::from_rgb(45, 28, 38); - pub const BG_MID: Color32 = Color32::from_rgb(55, 35, 45); - pub const BG_LIGHT: Color32 = Color32::from_rgb(70, 45, 58); - pub const BG_LIGHTER: Color32 = Color32::from_rgb(85, 55, 70); - - pub const BORDER_PINK: Color32 = Color32::from_rgb(85, 55, 70); - - pub const TEXT_PRIMARY: Color32 = Color32::from_rgb(255, 235, 245); - pub const TEXT_SECONDARY: Color32 = Color32::from_rgb(200, 160, 180); - pub const TEXT_MUTED: Color32 = Color32::from_rgb(150, 110, 130); - - pub const ACCENT_PINK: Color32 = Color32::from_rgb(255, 130, 180); - pub const ACCENT_HOT: Color32 = Color32::from_rgb(255, 90, 150); - pub const ACCENT_LIGHT: Color32 = Color32::from_rgb(255, 200, 220); + pub fn BG_DARKEST() -> Color32 { current_theme_colors().bg_darkest } + pub fn BG_DARK() -> Color32 { current_theme_colors().bg_dark } + pub fn BG_MID() -> Color32 { current_theme_colors().bg_mid } + pub fn BG_LIGHT() -> Color32 { current_theme_colors().bg_light } + pub fn BG_LIGHTER() -> Color32 { current_theme_colors().bg_lighter } + pub fn BORDER_PINK() -> Color32 { current_theme_colors().border } + pub fn TEXT_PRIMARY() -> Color32 { current_theme_colors().text_primary } + pub fn TEXT_SECONDARY() -> Color32 { current_theme_colors().text_secondary } + pub fn TEXT_MUTED() -> Color32 { current_theme_colors().text_muted } + pub fn ACCENT_PINK() -> Color32 { current_theme_colors().accent_primary } + pub fn ACCENT_HOT() -> Color32 { current_theme_colors().accent_hot } + pub fn ACCENT_LIGHT() -> Color32 { current_theme_colors().accent_light } pub fn apply(ctx: &egui::Context, corner_roundness: f32) { - let mut visuals = Visuals::dark(); + let colors = current_theme_colors(); + + let is_dark = colors.bg_darkest.r() < 128; + let mut visuals = if is_dark { + egui::Visuals::dark() + } else { + egui::Visuals::light() + }; let radius = egui::CornerRadius::same(corner_roundness.clamp(0.0, 255.0) as u8); - visuals.window_fill = Self::BG_DARK; - visuals.panel_fill = Self::BG_DARK; - visuals.window_stroke = egui::Stroke::new(1.0, Self::BG_LIGHT); + visuals.window_fill = colors.bg_dark; + visuals.panel_fill = colors.bg_dark; + visuals.window_stroke = egui::Stroke::new(1.0, colors.bg_light); visuals.window_corner_radius = radius; visuals.menu_corner_radius = radius; visuals.widgets.noninteractive.corner_radius = radius; - visuals.widgets.noninteractive.bg_fill = Self::BG_MID; + visuals.widgets.noninteractive.bg_fill = colors.bg_mid; visuals.widgets.inactive.corner_radius = radius; - visuals.widgets.inactive.bg_fill = Self::BG_LIGHT; + visuals.widgets.inactive.bg_fill = colors.bg_light; visuals.widgets.hovered.corner_radius = radius; - visuals.widgets.hovered.bg_fill = Self::BG_LIGHTER; + visuals.widgets.hovered.bg_fill = colors.bg_lighter; visuals.widgets.active.corner_radius = radius; - visuals.widgets.active.bg_fill = Self::PINK_700; + visuals.widgets.active.bg_fill = colors.accent_hot; visuals.widgets.open.corner_radius = radius; - visuals.widgets.open.bg_fill = Self::PINK_600; + visuals.widgets.open.bg_fill = colors.accent_primary; - visuals.selection.bg_fill = Self::PINK_600; - visuals.selection.stroke = egui::Stroke::new(1.0, Self::ACCENT_LIGHT); + visuals.selection.bg_fill = colors.accent_primary; + visuals.selection.stroke = egui::Stroke::new(1.0, colors.accent_light); - visuals.override_text_color = Some(Self::TEXT_PRIMARY); + visuals.override_text_color = Some(colors.text_primary); + visuals.hyperlink_color = colors.accent_primary; - visuals.hyperlink_color = Self::ACCENT_PINK; - - visuals.widgets.inactive.fg_stroke = egui::Stroke::new(1.0, Self::TEXT_PRIMARY); - visuals.widgets.hovered.fg_stroke = egui::Stroke::new(1.0, Self::ACCENT_LIGHT); - visuals.widgets.active.fg_stroke = egui::Stroke::new(1.0, Self::PINK_50); + visuals.widgets.inactive.fg_stroke = egui::Stroke::new(1.0, colors.text_primary); + visuals.widgets.hovered.fg_stroke = egui::Stroke::new(1.0, colors.accent_light); + visuals.widgets.active.fg_stroke = egui::Stroke::new(1.0, colors.bg_darkest); ctx.set_visuals(visuals); let mut style = (*ctx.global_style()).clone(); style.text_styles.insert( - TextStyle::Heading, - FontId::new(20.0, egui::FontFamily::Proportional), + egui::TextStyle::Heading, + egui::FontId::new(20.0, egui::FontFamily::Proportional), ); style.text_styles.insert( - TextStyle::Body, - FontId::new(14.0, egui::FontFamily::Proportional), + egui::TextStyle::Body, + egui::FontId::new(14.0, egui::FontFamily::Proportional), ); style.text_styles.insert( - TextStyle::Monospace, - FontId::new(13.0, egui::FontFamily::Monospace), + egui::TextStyle::Monospace, + egui::FontId::new(13.0, egui::FontFamily::Monospace), ); style.text_styles.insert( - TextStyle::Button, - FontId::new(13.0, egui::FontFamily::Proportional), + egui::TextStyle::Button, + egui::FontId::new(13.0, egui::FontFamily::Proportional), ); style.text_styles.insert( - TextStyle::Small, - FontId::new(11.0, egui::FontFamily::Proportional), + egui::TextStyle::Small, + egui::FontId::new(11.0, egui::FontFamily::Proportional), ); - ctx.set_global_style(style); } } + +pub fn apply_theme_from_settings(ctx: &egui::Context, variant: ThemeVariant, corner_roundness: f32) { + update_current_theme(variant); + CherryBlossomTheme::apply(ctx, corner_roundness); +} diff --git a/crates/theme/src/rose_pine.rs b/crates/theme/src/rose_pine.rs new file mode 100644 index 0000000..2ed07d2 --- /dev/null +++ b/crates/theme/src/rose_pine.rs @@ -0,0 +1,191 @@ +use egui::{Color32, FontId, TextStyle, Visuals}; + +fn apply_fonts(ctx: &egui::Context) { + let mut style = (*ctx.global_style()).clone(); + style.text_styles.insert( + TextStyle::Heading, + FontId::new(20.0, egui::FontFamily::Proportional), + ); + style.text_styles.insert( + TextStyle::Body, + FontId::new(14.0, egui::FontFamily::Proportional), + ); + style.text_styles.insert( + TextStyle::Monospace, + FontId::new(13.0, egui::FontFamily::Monospace), + ); + style.text_styles.insert( + TextStyle::Button, + FontId::new(13.0, egui::FontFamily::Proportional), + ); + style.text_styles.insert( + TextStyle::Small, + FontId::new(11.0, egui::FontFamily::Proportional), + ); + ctx.set_global_style(style); +} + +pub struct RosePine; + +impl RosePine { + pub const BASE: Color32 = Color32::from_rgb(250, 244, 237); + pub const SURFACE: Color32 = Color32::from_rgb(255, 250, 243); + pub const OVERLAY: Color32 = Color32::from_rgb(242, 233, 222); + pub const MUTED: Color32 = Color32::from_rgb(152, 147, 165); + pub const SUBTLE: Color32 = Color32::from_rgb(121, 112, 122); + pub const TEXT: Color32 = Color32::from_rgb(87, 82, 91); + pub const LOVE: Color32 = Color32::from_rgb(180, 99, 122); + pub const GOLD: Color32 = Color32::from_rgb(234, 157, 52); + pub const ROSE: Color32 = Color32::from_rgb(215, 130, 126); + pub const PINE: Color32 = Color32::from_rgb(40, 105, 131); + pub const FOAM: Color32 = Color32::from_rgb(86, 148, 159); + pub const IRIS: Color32 = Color32::from_rgb(144, 122, 169); + pub const HIGHLIGHT_LOW: Color32 = Color32::from_rgb(244, 237, 232); + pub const HIGHLIGHT_MED: Color32 = Color32::from_rgb(223, 218, 211); + pub const HIGHLIGHT_HIGH: Color32 = Color32::from_rgb(206, 202, 195); + + pub fn apply(ctx: &egui::Context, corner_roundness: f32) { + let mut visuals = Visuals::light(); + let radius = egui::CornerRadius::same(corner_roundness.clamp(0.0, 255.0) as u8); + + visuals.window_fill = Self::BASE; + visuals.panel_fill = Self::BASE; + visuals.window_stroke = egui::Stroke::new(1.0, Self::HIGHLIGHT_MED); + visuals.window_corner_radius = radius; + visuals.menu_corner_radius = radius; + + visuals.widgets.noninteractive.corner_radius = radius; + visuals.widgets.noninteractive.bg_fill = Self::SURFACE; + visuals.widgets.inactive.corner_radius = radius; + visuals.widgets.inactive.bg_fill = Self::OVERLAY; + visuals.widgets.hovered.corner_radius = radius; + visuals.widgets.hovered.bg_fill = Self::HIGHLIGHT_MED; + visuals.widgets.active.corner_radius = radius; + visuals.widgets.active.bg_fill = Self::ROSE; + visuals.widgets.open.corner_radius = radius; + visuals.widgets.open.bg_fill = Self::LOVE; + + visuals.selection.bg_fill = Self::ROSE; + visuals.selection.stroke = egui::Stroke::new(1.0, Self::LOVE); + + visuals.override_text_color = Some(Self::TEXT); + visuals.hyperlink_color = Self::IRIS; + + visuals.widgets.inactive.fg_stroke = egui::Stroke::new(1.0, Self::TEXT); + visuals.widgets.hovered.fg_stroke = egui::Stroke::new(1.0, Self::PINE); + visuals.widgets.active.fg_stroke = egui::Stroke::new(1.0, Self::BASE); + + ctx.set_visuals(visuals); + apply_fonts(ctx); + } +} + +pub struct RosePineMoon; + +impl RosePineMoon { + pub const BASE: Color32 = Color32::from_rgb(35, 33, 54); + pub const SURFACE: Color32 = Color32::from_rgb(42, 40, 62); + pub const OVERLAY: Color32 = Color32::from_rgb(57, 53, 82); + pub const MUTED: Color32 = Color32::from_rgb(110, 106, 134); + pub const SUBTLE: Color32 = Color32::from_rgb(144, 140, 170); + pub const TEXT: Color32 = Color32::from_rgb(224, 222, 244); + pub const LOVE: Color32 = Color32::from_rgb(235, 111, 146); + pub const GOLD: Color32 = Color32::from_rgb(246, 193, 119); + pub const ROSE: Color32 = Color32::from_rgb(235, 188, 186); + pub const PINE: Color32 = Color32::from_rgb(62, 143, 176); + pub const FOAM: Color32 = Color32::from_rgb(156, 207, 216); + pub const IRIS: Color32 = Color32::from_rgb(196, 167, 231); + pub const HIGHLIGHT_LOW: Color32 = Color32::from_rgb(42, 40, 62); + pub const HIGHLIGHT_MED: Color32 = Color32::from_rgb(68, 65, 90); + pub const HIGHLIGHT_HIGH: Color32 = Color32::from_rgb(82, 79, 103); + + pub fn apply(ctx: &egui::Context, corner_roundness: f32) { + let mut visuals = Visuals::dark(); + let radius = egui::CornerRadius::same(corner_roundness.clamp(0.0, 255.0) as u8); + + visuals.window_fill = Self::BASE; + visuals.panel_fill = Self::BASE; + visuals.window_stroke = egui::Stroke::new(1.0, Self::OVERLAY); + visuals.window_corner_radius = radius; + visuals.menu_corner_radius = radius; + + visuals.widgets.noninteractive.corner_radius = radius; + visuals.widgets.noninteractive.bg_fill = Self::SURFACE; + visuals.widgets.inactive.corner_radius = radius; + visuals.widgets.inactive.bg_fill = Self::OVERLAY; + visuals.widgets.hovered.corner_radius = radius; + visuals.widgets.hovered.bg_fill = Self::HIGHLIGHT_MED; + visuals.widgets.active.corner_radius = radius; + visuals.widgets.active.bg_fill = Self::LOVE; + visuals.widgets.open.corner_radius = radius; + visuals.widgets.open.bg_fill = Self::IRIS; + + visuals.selection.bg_fill = Self::LOVE; + visuals.selection.stroke = egui::Stroke::new(1.0, Self::ROSE); + + visuals.override_text_color = Some(Self::TEXT); + visuals.hyperlink_color = Self::FOAM; + + visuals.widgets.inactive.fg_stroke = egui::Stroke::new(1.0, Self::TEXT); + visuals.widgets.hovered.fg_stroke = egui::Stroke::new(1.0, Self::ROSE); + visuals.widgets.active.fg_stroke = egui::Stroke::new(1.0, Self::BASE); + + ctx.set_visuals(visuals); + apply_fonts(ctx); + } +} + +pub struct RosePineDawn; + +impl RosePineDawn { + pub const BASE: Color32 = Color32::from_rgb(250, 245, 238); + pub const SURFACE: Color32 = Color32::from_rgb(255, 251, 245); + pub const OVERLAY: Color32 = Color32::from_rgb(242, 237, 232); + pub const MUTED: Color32 = Color32::from_rgb(152, 147, 165); + pub const SUBTLE: Color32 = Color32::from_rgb(121, 112, 122); + pub const TEXT: Color32 = Color32::from_rgb(87, 82, 91); + pub const LOVE: Color32 = Color32::from_rgb(180, 99, 122); + pub const GOLD: Color32 = Color32::from_rgb(234, 157, 52); + pub const ROSE: Color32 = Color32::from_rgb(215, 130, 126); + pub const PINE: Color32 = Color32::from_rgb(40, 105, 131); + pub const FOAM: Color32 = Color32::from_rgb(86, 148, 159); + pub const IRIS: Color32 = Color32::from_rgb(144, 122, 169); + pub const HIGHLIGHT_LOW: Color32 = Color32::from_rgb(244, 239, 234); + pub const HIGHLIGHT_MED: Color32 = Color32::from_rgb(223, 218, 213); + pub const HIGHLIGHT_HIGH: Color32 = Color32::from_rgb(206, 202, 197); + + pub fn apply(ctx: &egui::Context, corner_roundness: f32) { + let mut visuals = Visuals::light(); + let radius = egui::CornerRadius::same(corner_roundness.clamp(0.0, 255.0) as u8); + + visuals.window_fill = Self::BASE; + visuals.panel_fill = Self::BASE; + visuals.window_stroke = egui::Stroke::new(1.0, Self::HIGHLIGHT_MED); + visuals.window_corner_radius = radius; + visuals.menu_corner_radius = radius; + + visuals.widgets.noninteractive.corner_radius = radius; + visuals.widgets.noninteractive.bg_fill = Self::SURFACE; + visuals.widgets.inactive.corner_radius = radius; + visuals.widgets.inactive.bg_fill = Self::OVERLAY; + visuals.widgets.hovered.corner_radius = radius; + visuals.widgets.hovered.bg_fill = Self::HIGHLIGHT_MED; + visuals.widgets.active.corner_radius = radius; + visuals.widgets.active.bg_fill = Self::ROSE; + visuals.widgets.open.corner_radius = radius; + visuals.widgets.open.bg_fill = Self::LOVE; + + visuals.selection.bg_fill = Self::ROSE; + visuals.selection.stroke = egui::Stroke::new(1.0, Self::LOVE); + + visuals.override_text_color = Some(Self::TEXT); + visuals.hyperlink_color = Self::IRIS; + + visuals.widgets.inactive.fg_stroke = egui::Stroke::new(1.0, Self::TEXT); + visuals.widgets.hovered.fg_stroke = egui::Stroke::new(1.0, Self::PINE); + visuals.widgets.active.fg_stroke = egui::Stroke::new(1.0, Self::BASE); + + ctx.set_visuals(visuals); + apply_fonts(ctx); + } +}