From 67e933153517b8dfcb552ca7a4c38848617db94b Mon Sep 17 00:00:00 2001 From: candle Date: Mon, 21 Jul 2025 20:28:09 -0400 Subject: [PATCH 1/9] syntax highlighting does kinda work but its slow --- Cargo.toml | 1 + src/app/config.rs | 6 +-- src/main.rs | 10 +++- src/ui/central_panel/editor.rs | 86 ++++++++++++++++++++++++++++++++++ src/ui/preferences_window.rs | 4 +- 5 files changed, 100 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8ee159f..b38d858 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] eframe = "0.32" egui = "0.32" +egui_extras = { version = "0.32", features = ["syntect"] } serde = { version = "1.0.219", features = ["derive"] } rfd = "0.15.4" toml = "0.9.2" diff --git a/src/app/config.rs b/src/app/config.rs index 7f14af5..1cba26d 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -65,11 +65,9 @@ impl Default for Config { impl Config { pub fn config_path() -> Option { let config_dir = if let Some(config_dir) = dirs::config_dir() { - config_dir.join(format!("{}", env!("CARGO_PKG_NAME"))) + config_dir.join(env!("CARGO_PKG_NAME")) } else if let Some(home_dir) = dirs::home_dir() { - home_dir - .join(".config") - .join(format!("{}", env!("CARGO_PKG_NAME"))) + home_dir.join(".config").join(env!("CARGO_PKG_NAME")) } else { return None; }; diff --git a/src/main.rs b/src/main.rs index 3621bd3..0a0716a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,21 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use eframe::egui; +use std::env; +use std::io::IsTerminal; mod app; mod io; mod ui; -use app::{config::Config, TextEditor}; +use app::{TextEditor, config::Config}; fn main() -> eframe::Result { + let _args: Vec = env::args().collect(); + if std::io::stdin().is_terminal() { + println!("This is a GUI application, are you sure you want to launch from terminal?"); + // return Ok(()); + } + let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_min_inner_size([600.0, 400.0]) diff --git a/src/ui/central_panel/editor.rs b/src/ui/central_panel/editor.rs index acd273a..24441ca 100644 --- a/src/ui/central_panel/editor.rs +++ b/src/ui/central_panel/editor.rs @@ -1,8 +1,76 @@ use crate::app::TextEditor; use eframe::egui; +use egui_extras::syntax_highlighting::{self, CodeTheme}; use super::find_highlight; +fn get_language_from_extension(file_path: Option<&std::path::Path>) -> String { + if let Some(path) = file_path { + if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) { + match extension.to_lowercase().as_str() { + "rs" => "rs".to_string(), + "py" => "py".to_string(), + "js" => "js".to_string(), + "ts" => "ts".to_string(), + "tsx" => "tsx".to_string(), + "jsx" => "jsx".to_string(), + "c" => "c".to_string(), + "cpp" | "cc" | "cxx" => "cpp".to_string(), + "h" | "hpp" => "cpp".to_string(), + "java" => "java".to_string(), + "go" => "go".to_string(), + "php" => "php".to_string(), + "rb" => "rb".to_string(), + "cs" => "cs".to_string(), + "swift" => "swift".to_string(), + "kt" => "kt".to_string(), + "scala" => "scala".to_string(), + "sh" | "bash" | "zsh" | "fish" => "sh".to_string(), + "html" | "htm" => "html".to_string(), + "xml" => "xml".to_string(), + "css" => "css".to_string(), + "scss" | "sass" => "scss".to_string(), + "json" => "json".to_string(), + "yaml" | "yml" => "yaml".to_string(), + "toml" => "toml".to_string(), + "md" | "markdown" => "md".to_string(), + "sql" => "sql".to_string(), + "lua" => "lua".to_string(), + "vim" => "vim".to_string(), + "dockerfile" => "dockerfile".to_string(), + "makefile" => "makefile".to_string(), + _ => "txt".to_string(), + } + } else { + // Check filename for special cases + if let Some(filename) = path.file_name().and_then(|name| name.to_str()) { + match filename.to_lowercase().as_str() { + "dockerfile" => "dockerfile".to_string(), + "makefile" => "makefile".to_string(), + "cargo.toml" | "pyproject.toml" => "toml".to_string(), + "package.json" => "json".to_string(), + _ => "txt".to_string(), + } + } else { + "txt".to_string() + } + } + } else { + "txt".to_string() + } +} + +fn create_code_theme_from_visuals(visuals: &egui::Visuals, font_size: f32) -> CodeTheme { + // For now, just use the appropriate base theme (dark/light) + // The base themes should automatically work well with the system colors + // since egui's syntax highlighting respects the overall UI theme + if visuals.dark_mode { + CodeTheme::dark(font_size) + } else { + CodeTheme::light(font_size) + } +} + pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::Response { let _current_match_position = app.get_current_match_position(); let show_find = app.show_find; @@ -96,6 +164,23 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R f32::INFINITY }; + // Determine the language for syntax highlighting + let language = get_language_from_extension(active_tab.file_path.as_deref()); + + // Create a code theme based on the current system theme visuals + let theme = create_code_theme_from_visuals(ui.visuals(), font_size); + + let mut layouter = |ui: &egui::Ui, string: &dyn egui::TextBuffer, wrap_width: f32| { + let text = string.as_str(); + let mut layout_job = if language == "txt" { + syntax_highlighting::highlight(ui.ctx(), &ui.style().clone(), &theme, text, "") + } else { + syntax_highlighting::highlight(ui.ctx(), &ui.style().clone(), &theme, text, &language) + }; + layout_job.wrap.max_width = wrap_width; + ui.fonts(|f| f.layout_job(layout_job)) + }; + let text_edit = egui::TextEdit::multiline(&mut active_tab.content) .frame(false) .font(egui::TextStyle::Monospace) @@ -104,6 +189,7 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R .desired_rows(0) .lock_focus(!show_find) .cursor_at_end(false) + .layouter(&mut layouter) .id(egui::Id::new("main_text_editor")); let output = if word_wrap { diff --git a/src/ui/preferences_window.rs b/src/ui/preferences_window.rs index 3ecd285..c888e6e 100644 --- a/src/ui/preferences_window.rs +++ b/src/ui/preferences_window.rs @@ -4,8 +4,8 @@ use eframe::egui; pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) { let visuals = &ctx.style().visuals; let screen_rect = ctx.screen_rect(); - let window_width = (screen_rect.width() * 0.6).min(400.0).max(300.0); - let window_height = (screen_rect.height() * 0.7).min(500.0).max(250.0); + let window_width = (screen_rect.width() * 0.6).clamp(300.0, 400.0); + let window_height = (screen_rect.height() * 0.7).clamp(250.0, 500.0); let max_size = egui::Vec2::new(window_width, window_height); egui::Window::new("Preferences") -- 2.47.1 From c4de8acec5af9c1d4d6f2561b8f8e19d25401e00 Mon Sep 17 00:00:00 2001 From: candle Date: Tue, 22 Jul 2025 22:26:48 -0400 Subject: [PATCH 2/9] can click and drag tab bar now, fix hashing, unification --- src/app/tab.rs | 30 +++----- src/ui/central_panel/editor.rs | 5 -- src/ui/menu_bar.rs | 10 +-- src/ui/tab_bar.rs | 136 +++++++++++++++++---------------- 4 files changed, 84 insertions(+), 97 deletions(-) diff --git a/src/app/tab.rs b/src/app/tab.rs index 2e34888..fcd0253 100644 --- a/src/app/tab.rs +++ b/src/app/tab.rs @@ -2,35 +2,32 @@ use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use std::path::PathBuf; -pub fn compute_content_hash(content: &str, hasher: &mut DefaultHasher) -> u64 { - content.hash(hasher); - hasher.finish() +pub fn compute_content_hash(content: &str) -> u64 { + let mut hasher = DefaultHasher::new(); + content.hash(&mut hasher); + let hash = hasher.finish(); + hash } #[derive(Clone)] pub struct Tab { pub content: String, pub original_content_hash: u64, - pub last_content_hash: u64, pub file_path: Option, pub is_modified: bool, pub title: String, - pub hasher: DefaultHasher, } impl Tab { pub fn new_empty(tab_number: usize) -> Self { let content = String::new(); - let mut hasher = DefaultHasher::new(); - let hash = compute_content_hash(&content, &mut hasher); + let hash = compute_content_hash(&content); Self { original_content_hash: hash, - last_content_hash: hash, content, file_path: None, is_modified: false, title: format!("new_{tab_number}"), - hasher, } } @@ -38,19 +35,16 @@ impl Tab { let title = file_path .file_name() .and_then(|n| n.to_str()) - .unwrap_or("Untitled") + .unwrap_or("UNKNOWN") .to_string(); - let mut hasher = DefaultHasher::new(); - let hash = compute_content_hash(&content, &mut hasher); + let hash = compute_content_hash(&content); Self { original_content_hash: hash, - last_content_hash: hash, content, file_path: Some(file_path), is_modified: false, title, - hasher, } } @@ -63,15 +57,13 @@ impl Tab { if self.title.starts_with("new_") { self.is_modified = !self.content.is_empty(); } else { - let current_hash = compute_content_hash(&self.content, &mut self.hasher); - self.is_modified = current_hash != self.last_content_hash; - self.last_content_hash = current_hash; + let current_hash = compute_content_hash(&self.content); + self.is_modified = current_hash != self.original_content_hash; } } pub fn mark_as_saved(&mut self) { - self.original_content_hash = compute_content_hash(&self.content, &mut self.hasher); - self.last_content_hash = self.original_content_hash; + self.original_content_hash = compute_content_hash(&self.content); self.is_modified = false; } } diff --git a/src/ui/central_panel/editor.rs b/src/ui/central_panel/editor.rs index 24441ca..a9302f6 100644 --- a/src/ui/central_panel/editor.rs +++ b/src/ui/central_panel/editor.rs @@ -248,11 +248,6 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R app.previous_content = content.to_owned(); app.previous_cursor_char_index = current_cursor_pos; - - if let Some(active_tab) = app.get_active_tab_mut() { - active_tab.last_content_hash = - crate::app::tab::compute_content_hash(&active_tab.content, &mut active_tab.hasher); - } } if app.font_settings_changed || app.text_needs_processing { diff --git a/src/ui/menu_bar.rs b/src/ui/menu_bar.rs index 268840e..15dc49e 100644 --- a/src/ui/menu_bar.rs +++ b/src/ui/menu_bar.rs @@ -276,21 +276,15 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) { if app.hide_tab_bar { let tab_title = if let Some(tab) = app.get_active_tab() { - tab.title.to_owned() + tab.get_display_title() } else { let empty_tab = crate::app::tab::Tab::new_empty(1); - empty_tab.title.to_owned() + empty_tab.get_display_title() }; let window_width = ctx.screen_rect().width(); let font_id = ui.style().text_styles[&egui::TextStyle::Body].to_owned(); - let tab_title = if app.get_active_tab().is_some_and(|tab| tab.is_modified) { - format!("{tab_title}*") - } else { - tab_title - }; - let text_galley = ui.fonts(|fonts| { fonts.layout_job(egui::text::LayoutJob::simple_singleline( tab_title, diff --git a/src/ui/tab_bar.rs b/src/ui/tab_bar.rs index d09b297..86f631b 100644 --- a/src/ui/tab_bar.rs +++ b/src/ui/tab_bar.rs @@ -3,86 +3,92 @@ use eframe::egui::{self, Frame}; pub(crate) fn tab_bar(app: &mut TextEditor, ctx: &egui::Context) { let frame = Frame::NONE.fill(ctx.style().visuals.panel_fill); - let response = egui::TopBottomPanel::top("tab_bar") + let tab_bar = egui::TopBottomPanel::top("tab_bar") .frame(frame) .show(ctx, |ui| { - ui.horizontal(|ui| { - let mut tab_to_close_unmodified = None; - let mut tab_to_close_modified = None; - let mut tab_to_switch = None; - let mut add_new_tab = false; + egui::ScrollArea::horizontal() + .auto_shrink([false, true]) + .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden) + .scroll_source(egui::scroll_area::ScrollSource::DRAG) + .show(ui, |ui| { + ui.horizontal(|ui| { + let mut tab_to_close_unmodified = None; + let mut tab_to_close_modified = None; + let mut tab_to_switch = None; + let mut add_new_tab = false; - let tabs_len = app.tabs.len(); - let active_tab_index = app.active_tab_index; + let tabs_len = app.tabs.len(); + let active_tab_index = app.active_tab_index; - let tabs_info: Vec<(String, bool)> = app - .tabs - .iter() - .map(|tab| (tab.get_display_title(), tab.is_modified)) - .collect(); + let tabs_info: Vec<(String, bool)> = app + .tabs + .iter() + .map(|tab| (tab.get_display_title(), tab.is_modified)) + .collect(); - for (i, (title, is_modified)) in tabs_info.iter().enumerate() { - let is_active = i == active_tab_index; + for (i, (title, is_modified)) in tabs_info.iter().enumerate() { + let is_active = i == active_tab_index; - let mut label_text = if is_active { - egui::RichText::new(title).strong() - } else { - egui::RichText::new(title).color(ui.visuals().weak_text_color()) - }; + let mut label_text = if is_active { + egui::RichText::new(title).strong() + } else { + egui::RichText::new(title).color(ui.visuals().weak_text_color()) + }; - if *is_modified { - label_text = label_text.italics(); - } + if *is_modified { + label_text = label_text.italics(); + } - let tab_response = - ui.add(egui::Label::new(label_text).sense(egui::Sense::click())); - if tab_response.clicked() { - tab_to_switch = Some(i); - } + let tab_response = ui.add(egui::Label::new(label_text).selectable(false).sense(egui::Sense::click())); + if tab_response.clicked() { + tab_to_switch = Some(i); + } + + if tabs_len > 1 { + let visuals = ui.visuals(); + let close_button = egui::Button::new("×") + .small() + .fill(visuals.panel_fill) + .stroke(egui::Stroke::new(0.0, egui::Color32::from_rgb(0, 0, 0))); + let close_response = ui.add(close_button); + if close_response.clicked() { + if *is_modified { + tab_to_close_modified = Some(i); + } else { + tab_to_close_unmodified = Some(i); + } + } + } + + ui.separator(); + } - if tabs_len > 1 { let visuals = ui.visuals(); - let close_button = egui::Button::new("×") + let add_button = egui::Button::new("+") .small() .fill(visuals.panel_fill) .stroke(egui::Stroke::new(0.0, egui::Color32::from_rgb(0, 0, 0))); - let close_response = ui.add(close_button); - if close_response.clicked() { - if *is_modified { - tab_to_close_modified = Some(i); - } else { - tab_to_close_unmodified = Some(i); - } + if ui.add(add_button).clicked() { + add_new_tab = true; } - } - ui.separator(); - } - - let visuals = ui.visuals(); - let add_button = egui::Button::new("+") - .small() - .fill(visuals.panel_fill) - .stroke(egui::Stroke::new(0.0, egui::Color32::from_rgb(0, 0, 0))); - if ui.add(add_button).clicked() { - add_new_tab = true; - } - - if let Some(tab_index) = tab_to_switch { - app.switch_to_tab(tab_index); - } - if let Some(tab_index) = tab_to_close_unmodified { - app.close_tab(tab_index); - } - if let Some(tab_index) = tab_to_close_modified { - app.switch_to_tab(tab_index); - app.pending_unsaved_action = Some(UnsavedAction::CloseTab(tab_index)); - } - if add_new_tab { - app.add_new_tab(); - } - }); + if let Some(tab_index) = tab_to_switch { + app.switch_to_tab(tab_index); + } + if let Some(tab_index) = tab_to_close_unmodified { + app.close_tab(tab_index); + } + if let Some(tab_index) = tab_to_close_modified { + app.switch_to_tab(tab_index); + app.pending_unsaved_action = Some(UnsavedAction::CloseTab(tab_index)); + } + if add_new_tab { + app.add_new_tab(); + } + }); + }); }); - app.tab_bar_rect = Some(response.response.rect); + app.tab_bar_rect = Some(tab_bar.response.rect); + } -- 2.47.1 From c50c9b67792914011141f37121fd544ccb085a6e Mon Sep 17 00:00:00 2001 From: candle Date: Tue, 22 Jul 2025 22:49:54 -0400 Subject: [PATCH 3/9] syntax highlighting toggle, uses your font now --- src/ui/central_panel/editor.rs | 19 ++++++++++++------- src/ui/menu_bar.rs | 4 ++++ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/ui/central_panel/editor.rs b/src/ui/central_panel/editor.rs index a9302f6..2b8fdf3 100644 --- a/src/ui/central_panel/editor.rs +++ b/src/ui/central_panel/editor.rs @@ -80,6 +80,8 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R let show_shortcuts = app.show_shortcuts; let word_wrap = app.word_wrap; let font_size = app.font_size; + let font_id = app.get_font_id(); + let syntax_highlighting_enabled = app.syntax_highlighting; let reset_zoom_key = egui::Id::new("editor_reset_zoom"); let should_reset_zoom = ui @@ -164,26 +166,29 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R f32::INFINITY }; - // Determine the language for syntax highlighting let language = get_language_from_extension(active_tab.file_path.as_deref()); - - // Create a code theme based on the current system theme visuals let theme = create_code_theme_from_visuals(ui.visuals(), font_size); let mut layouter = |ui: &egui::Ui, string: &dyn egui::TextBuffer, wrap_width: f32| { let text = string.as_str(); - let mut layout_job = if language == "txt" { - syntax_highlighting::highlight(ui.ctx(), &ui.style().clone(), &theme, text, "") - } else { + let mut layout_job = if syntax_highlighting_enabled && language != "txt" { syntax_highlighting::highlight(ui.ctx(), &ui.style().clone(), &theme, text, &language) + } else { + syntax_highlighting::highlight(ui.ctx(), &ui.style().clone(), &theme, text, "") }; + + if syntax_highlighting_enabled && language != "txt" { + for section in &mut layout_job.sections { + section.format.font_id = font_id.clone(); + } + } + layout_job.wrap.max_width = wrap_width; ui.fonts(|f| f.layout_job(layout_job)) }; let text_edit = egui::TextEdit::multiline(&mut active_tab.content) .frame(false) - .font(egui::TextStyle::Monospace) .code_editor() .desired_width(desired_width) .desired_rows(0) diff --git a/src/ui/menu_bar.rs b/src/ui/menu_bar.rs index 15dc49e..a7ff6cc 100644 --- a/src/ui/menu_bar.rs +++ b/src/ui/menu_bar.rs @@ -185,6 +185,10 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) { app.save_config(); ui.close_kind(UiKind::Menu); } + if ui.checkbox(&mut app.syntax_highlighting, "Syntax Highlighting").clicked() { + app.save_config(); + ui.close_kind(UiKind::Menu); + } if ui.checkbox(&mut app.word_wrap, "Word Wrap").clicked() { app.save_config(); ui.close_kind(UiKind::Menu); -- 2.47.1 From fd26344b5fb96dc757c3dc7aa523d2151b6d1e0e Mon Sep 17 00:00:00 2001 From: candle Date: Tue, 22 Jul 2025 23:09:01 -0400 Subject: [PATCH 4/9] moving lanugage/theme stuff out --- src/app/theme.rs | 10 +++++ src/ui/central_panel.rs | 1 + src/ui/central_panel/editor.rs | 72 ++----------------------------- src/ui/central_panel/languages.rs | 55 +++++++++++++++++++++++ 4 files changed, 69 insertions(+), 69 deletions(-) create mode 100644 src/ui/central_panel/languages.rs diff --git a/src/app/theme.rs b/src/app/theme.rs index 7761053..52d1f6d 100644 --- a/src/app/theme.rs +++ b/src/app/theme.rs @@ -1,4 +1,5 @@ use eframe::egui; +use egui_extras::syntax_highlighting::CodeTheme; #[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize, Default)] pub enum Theme { @@ -194,3 +195,12 @@ fn detect_system_dark_mode() -> bool { true } } + +pub fn create_code_theme_from_visuals(visuals: &egui::Visuals, font_size: f32) -> CodeTheme { + if visuals.dark_mode { + CodeTheme::dark(font_size) + } else { + CodeTheme::light(font_size) + } +} + diff --git a/src/ui/central_panel.rs b/src/ui/central_panel.rs index 5ebab4e..6f6e7e6 100644 --- a/src/ui/central_panel.rs +++ b/src/ui/central_panel.rs @@ -1,6 +1,7 @@ mod editor; mod find_highlight; mod line_numbers; +mod languages; use crate::app::TextEditor; use eframe::egui; diff --git a/src/ui/central_panel/editor.rs b/src/ui/central_panel/editor.rs index 2b8fdf3..0995422 100644 --- a/src/ui/central_panel/editor.rs +++ b/src/ui/central_panel/editor.rs @@ -1,75 +1,9 @@ use crate::app::TextEditor; use eframe::egui; -use egui_extras::syntax_highlighting::{self, CodeTheme}; +use egui_extras::syntax_highlighting::{self}; use super::find_highlight; -fn get_language_from_extension(file_path: Option<&std::path::Path>) -> String { - if let Some(path) = file_path { - if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) { - match extension.to_lowercase().as_str() { - "rs" => "rs".to_string(), - "py" => "py".to_string(), - "js" => "js".to_string(), - "ts" => "ts".to_string(), - "tsx" => "tsx".to_string(), - "jsx" => "jsx".to_string(), - "c" => "c".to_string(), - "cpp" | "cc" | "cxx" => "cpp".to_string(), - "h" | "hpp" => "cpp".to_string(), - "java" => "java".to_string(), - "go" => "go".to_string(), - "php" => "php".to_string(), - "rb" => "rb".to_string(), - "cs" => "cs".to_string(), - "swift" => "swift".to_string(), - "kt" => "kt".to_string(), - "scala" => "scala".to_string(), - "sh" | "bash" | "zsh" | "fish" => "sh".to_string(), - "html" | "htm" => "html".to_string(), - "xml" => "xml".to_string(), - "css" => "css".to_string(), - "scss" | "sass" => "scss".to_string(), - "json" => "json".to_string(), - "yaml" | "yml" => "yaml".to_string(), - "toml" => "toml".to_string(), - "md" | "markdown" => "md".to_string(), - "sql" => "sql".to_string(), - "lua" => "lua".to_string(), - "vim" => "vim".to_string(), - "dockerfile" => "dockerfile".to_string(), - "makefile" => "makefile".to_string(), - _ => "txt".to_string(), - } - } else { - // Check filename for special cases - if let Some(filename) = path.file_name().and_then(|name| name.to_str()) { - match filename.to_lowercase().as_str() { - "dockerfile" => "dockerfile".to_string(), - "makefile" => "makefile".to_string(), - "cargo.toml" | "pyproject.toml" => "toml".to_string(), - "package.json" => "json".to_string(), - _ => "txt".to_string(), - } - } else { - "txt".to_string() - } - } - } else { - "txt".to_string() - } -} - -fn create_code_theme_from_visuals(visuals: &egui::Visuals, font_size: f32) -> CodeTheme { - // For now, just use the appropriate base theme (dark/light) - // The base themes should automatically work well with the system colors - // since egui's syntax highlighting respects the overall UI theme - if visuals.dark_mode { - CodeTheme::dark(font_size) - } else { - CodeTheme::light(font_size) - } -} pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::Response { let _current_match_position = app.get_current_match_position(); @@ -166,8 +100,8 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R f32::INFINITY }; - let language = get_language_from_extension(active_tab.file_path.as_deref()); - let theme = create_code_theme_from_visuals(ui.visuals(), font_size); + let language = super::languages::get_language_from_extension(active_tab.file_path.as_deref()); + let theme = crate::app::theme::create_code_theme_from_visuals(ui.visuals(), font_size); let mut layouter = |ui: &egui::Ui, string: &dyn egui::TextBuffer, wrap_width: f32| { let text = string.as_str(); diff --git a/src/ui/central_panel/languages.rs b/src/ui/central_panel/languages.rs new file mode 100644 index 0000000..73525c8 --- /dev/null +++ b/src/ui/central_panel/languages.rs @@ -0,0 +1,55 @@ +pub fn get_language_from_extension(file_path: Option<&std::path::Path>) -> String { + if let Some(path) = file_path { + if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) { + match extension.to_lowercase().as_str() { + "rs" => "rs".to_string(), + "py" => "py".to_string(), + "js" => "js".to_string(), + "ts" => "ts".to_string(), + "tsx" => "tsx".to_string(), + "jsx" => "jsx".to_string(), + "c" => "c".to_string(), + "cpp" | "cc" | "cxx" => "cpp".to_string(), + "h" | "hpp" => "cpp".to_string(), + "java" => "java".to_string(), + "go" => "go".to_string(), + "php" => "php".to_string(), + "rb" => "rb".to_string(), + "cs" => "cs".to_string(), + "swift" => "swift".to_string(), + "kt" => "kt".to_string(), + "scala" => "scala".to_string(), + "sh" | "bash" | "zsh" | "fish" => "sh".to_string(), + "html" | "htm" => "html".to_string(), + "xml" => "xml".to_string(), + "css" => "css".to_string(), + "scss" | "sass" => "scss".to_string(), + "json" => "json".to_string(), + "yaml" | "yml" => "yaml".to_string(), + "toml" => "toml".to_string(), + "md" | "markdown" => "md".to_string(), + "sql" => "sql".to_string(), + "lua" => "lua".to_string(), + "vim" => "vim".to_string(), + "dockerfile" => "dockerfile".to_string(), + "makefile" => "makefile".to_string(), + _ => "txt".to_string(), + } + } else { + // Check filename for special cases + if let Some(filename) = path.file_name().and_then(|name| name.to_str()) { + match filename.to_lowercase().as_str() { + "dockerfile" => "dockerfile".to_string(), + "makefile" => "makefile".to_string(), + "cargo.toml" | "pyproject.toml" => "toml".to_string(), + "package.json" => "json".to_string(), + _ => "txt".to_string(), + } + } else { + "txt".to_string() + } + } + } else { + "txt".to_string() + } +} -- 2.47.1 From 4651d7caf4a6cbe25c1130acab19968c8223c877 Mon Sep 17 00:00:00 2001 From: candle Date: Wed, 23 Jul 2025 11:46:54 -0400 Subject: [PATCH 5/9] checkpoint for theming --- Cargo.toml | 2 + README.md | 5 +- src/app/theme.rs | 111 ++++++++++++++++++++++++++++-- src/ui/central_panel/editor.rs | 13 ++-- src/ui/central_panel/languages.rs | 100 +++++++++++++-------------- 5 files changed, 168 insertions(+), 63 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b38d858..7af4b35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,5 @@ rfd = "0.15.4" toml = "0.9.2" dirs = "6.0" libc = "0.2.174" +syntect = "5.2.0" +plist = "1.7.4" diff --git a/README.md b/README.md index 10272ef..50117b3 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ theme = "System" line_side = false font_family = "Monospace" font_size = 16.0 +syntax_highlighting = true ``` ### Options @@ -55,6 +56,7 @@ font_size = 16.0 | `auto_hide_toolbar` | `false` | If `true`, the menu bar at the top will be hidden. Move your mouse to the top of the window to reveal it. | | `hide_tab_bar` | 'true' | If `false`, a separate tab bar will be drawn below the toolbar. | | `show_line_numbers` | `false` | If `true`, line numbers will be displayed on the side specified by `line_side`. | +| `syntax_highlighting` | `false` | If `true`, text will be highlighted based on detected language. | | `line_side` | `false` | If `false`, line numbers are on the left. If `true`, they are on the right. | | `word_wrap` | `false` | If `true`, lines will wrap when they reach the edge of the window. | | `font_family` | `"Proportional"` | The font family used for the editor text. | @@ -65,9 +67,8 @@ font_size = 16.0 In order of importance. | Feature | Info | | ------- | ---- | -| **Find/Replace:** | Functioning. | | **State/Cache:** | A toggleable option to keep an application state and prevent "Quit without saving" warnings. | -| **Syntax Highlighting/LSP:** | Looking at allowing you to use/attach your own tools for this. | +| **LSP:** | Looking at allowing you to use/attach your own tools for this. | | **Choose Font** | More than just Monospace/Proportional. | | **Vim Mode:** | It's in-escapable. | | **CLI Mode:** | 💀 | diff --git a/src/app/theme.rs b/src/app/theme.rs index 52d1f6d..0a6dac4 100644 --- a/src/app/theme.rs +++ b/src/app/theme.rs @@ -1,5 +1,7 @@ use eframe::egui; -use egui_extras::syntax_highlighting::CodeTheme; +use plist::{Dictionary, Value}; +use std::collections::BTreeMap; +use syntect::highlighting::{Theme as SyntectTheme, ThemeSet, ThemeSettings, Color as SyntectColor, UnderlineOption}; #[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize, Default)] pub enum Theme { @@ -196,11 +198,108 @@ fn detect_system_dark_mode() -> bool { } } -pub fn create_code_theme_from_visuals(visuals: &egui::Visuals, font_size: f32) -> CodeTheme { - if visuals.dark_mode { - CodeTheme::dark(font_size) - } else { - CodeTheme::light(font_size) +fn egui_color_to_syntect(color: egui::Color32) -> SyntectColor { + SyntectColor { + r: color.r(), + g: color.g(), + b: color.b(), + a: color.a(), } } +pub fn create_code_theme_from_visuals(visuals: &egui::Visuals, font_size: f32) -> ThemeSet { + let text_color = visuals.override_text_color.unwrap_or(visuals.text_color()); + let bg_color = visuals.extreme_bg_color; + let selection_color = visuals.selection.bg_fill; + let comment_color = blend_colors(text_color, bg_color, 0.6); + let keyword_color = if visuals.dark_mode { + blend_colors(egui::Color32::from_rgb(100, 149, 237), text_color, 0.8) // CornflowerBlue-like + } else { + blend_colors(egui::Color32::from_rgb(0, 0, 139), text_color, 0.8) // DarkBlue-like + }; + let string_color = if visuals.dark_mode { + blend_colors(egui::Color32::from_rgb(144, 238, 144), text_color, 0.8) // LightGreen-like + } else { + blend_colors(egui::Color32::from_rgb(0, 128, 0), text_color, 0.8) // Green-like + }; + let number_color = if visuals.dark_mode { + blend_colors(egui::Color32::from_rgb(255, 165, 0), text_color, 0.8) // Orange-like + } else { + blend_colors(egui::Color32::from_rgb(165, 42, 42), text_color, 0.8) // Brown-like + }; + let function_color = if visuals.dark_mode { + blend_colors(egui::Color32::from_rgb(255, 20, 147), text_color, 0.8) // DeepPink-like + } else { + blend_colors(egui::Color32::from_rgb(128, 0, 128), text_color, 0.8) // Purple-like + }; + + let plist_theme = build_custom_theme_plist("System", &format!("{:?}", bg_color), &format!("{:?}", text_color), &format!("{:?}", comment_color), &format!("{:?}", string_color), &format!("{:?}", keyword_color)); + let file = std::fs::File::create("system.tmTheme").unwrap(); + let writer = std::io::BufWriter::new(file); + + let _ =plist::to_writer_xml(writer, &plist_theme); + + let loaded_file = std::fs::File::open("system.tmTheme").unwrap(); + let mut loaded_reader = std::io::BufReader::new(loaded_file); + let loaded_theme = ThemeSet::load_from_reader(&mut loaded_reader).unwrap(); + let mut set = ThemeSet::new(); + set.add_from_folder(".").unwrap(); + return set; + +} + +fn build_custom_theme_plist( + theme_name: &str, + background_color: &str, + foreground_color: &str, + comment_color: &str, + string_color: &str, + keyword_color: &str, +) -> Value { + let mut root_dict = Dictionary::new(); + root_dict.insert("name".to_string(), Value::String(theme_name.to_string())); + + let mut settings_array = Vec::new(); + + // Global settings + let mut global_settings_dict = Dictionary::new(); + let mut inner_global_settings = Dictionary::new(); + inner_global_settings.insert("background".to_string(), Value::String(background_color.to_string())); + inner_global_settings.insert("foreground".to_string(), Value::String(foreground_color.to_string())); + global_settings_dict.insert("settings".to_string(), Value::Dictionary(inner_global_settings)); + settings_array.push(Value::Dictionary(global_settings_dict)); + + // Comment scope + let mut comment_scope_dict = Dictionary::new(); + comment_scope_dict.insert("name".to_string(), Value::String("Comment".to_string())); + comment_scope_dict.insert("scope".to_string(), Value::String("comment".to_string())); + let mut comment_settings = Dictionary::new(); + comment_settings.insert("foreground".to_string(), Value::String(comment_color.to_string())); + comment_settings.insert("fontStyle".to_string(), Value::String("italic".to_string())); + comment_scope_dict.insert("settings".to_string(), Value::Dictionary(comment_settings)); + settings_array.push(Value::Dictionary(comment_scope_dict)); + + // String scope + let mut string_scope_dict = Dictionary::new(); + string_scope_dict.insert("name".to_string(), Value::String("String".to_string())); + string_scope_dict.insert("scope".to_string(), Value::String("string".to_string())); + let mut string_settings = Dictionary::new(); + string_settings.insert("foreground".to_string(), Value::String(string_color.to_string())); + string_scope_dict.insert("settings".to_string(), Value::Dictionary(string_settings)); + settings_array.push(Value::Dictionary(string_scope_dict)); + + // Keyword scope + let mut keyword_scope_dict = Dictionary::new(); + keyword_scope_dict.insert("name".to_string(), Value::String("Keyword".to_string())); + keyword_scope_dict.insert("scope".to_string(), Value::String("keyword".to_string())); + let mut keyword_settings = Dictionary::new(); + keyword_settings.insert("foreground".to_string(), Value::String(keyword_color.to_string())); + keyword_scope_dict.insert("settings".to_string(), Value::Dictionary(keyword_settings)); + settings_array.push(Value::Dictionary(keyword_scope_dict)); + + // Add more scopes as needed... + + root_dict.insert("settings".to_string(), Value::Array(settings_array)); + + Value::Dictionary(root_dict) +} \ No newline at end of file diff --git a/src/ui/central_panel/editor.rs b/src/ui/central_panel/editor.rs index 0995422..fb8867b 100644 --- a/src/ui/central_panel/editor.rs +++ b/src/ui/central_panel/editor.rs @@ -4,7 +4,6 @@ use egui_extras::syntax_highlighting::{self}; use super::find_highlight; - pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::Response { let _current_match_position = app.get_current_match_position(); let show_find = app.show_find; @@ -101,22 +100,26 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R }; let language = super::languages::get_language_from_extension(active_tab.file_path.as_deref()); - let theme = crate::app::theme::create_code_theme_from_visuals(ui.visuals(), font_size); - let mut layouter = |ui: &egui::Ui, string: &dyn egui::TextBuffer, wrap_width: f32| { + // let syntect_theme = + // crate::app::theme::create_code_theme_from_visuals(ui.visuals(), font_size); let text = string.as_str(); + let theme = egui_extras::syntax_highlighting::CodeTheme::dark(font_size); let mut layout_job = if syntax_highlighting_enabled && language != "txt" { + // let mut settings = egui_extras::syntax_highlighting::SyntectSettings::default(); + // settings.ts = syntect_theme; + // syntax_highlighting::highlight_with(ui.ctx(), &ui.style().clone(), &theme, text, &language, &settings) syntax_highlighting::highlight(ui.ctx(), &ui.style().clone(), &theme, text, &language) } else { syntax_highlighting::highlight(ui.ctx(), &ui.style().clone(), &theme, text, "") }; - + if syntax_highlighting_enabled && language != "txt" { for section in &mut layout_job.sections { section.format.font_id = font_id.clone(); } } - + layout_job.wrap.max_width = wrap_width; ui.fonts(|f| f.layout_job(layout_job)) }; diff --git a/src/ui/central_panel/languages.rs b/src/ui/central_panel/languages.rs index 73525c8..d95574a 100644 --- a/src/ui/central_panel/languages.rs +++ b/src/ui/central_panel/languages.rs @@ -1,55 +1,55 @@ pub fn get_language_from_extension(file_path: Option<&std::path::Path>) -> String { - if let Some(path) = file_path { - if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) { - match extension.to_lowercase().as_str() { - "rs" => "rs".to_string(), - "py" => "py".to_string(), - "js" => "js".to_string(), - "ts" => "ts".to_string(), - "tsx" => "tsx".to_string(), - "jsx" => "jsx".to_string(), - "c" => "c".to_string(), - "cpp" | "cc" | "cxx" => "cpp".to_string(), - "h" | "hpp" => "cpp".to_string(), - "java" => "java".to_string(), - "go" => "go".to_string(), - "php" => "php".to_string(), - "rb" => "rb".to_string(), - "cs" => "cs".to_string(), - "swift" => "swift".to_string(), - "kt" => "kt".to_string(), - "scala" => "scala".to_string(), - "sh" | "bash" | "zsh" | "fish" => "sh".to_string(), - "html" | "htm" => "html".to_string(), - "xml" => "xml".to_string(), - "css" => "css".to_string(), - "scss" | "sass" => "scss".to_string(), - "json" => "json".to_string(), - "yaml" | "yml" => "yaml".to_string(), - "toml" => "toml".to_string(), - "md" | "markdown" => "md".to_string(), - "sql" => "sql".to_string(), - "lua" => "lua".to_string(), - "vim" => "vim".to_string(), - "dockerfile" => "dockerfile".to_string(), - "makefile" => "makefile".to_string(), - _ => "txt".to_string(), - } - } else { - // Check filename for special cases - if let Some(filename) = path.file_name().and_then(|name| name.to_str()) { - match filename.to_lowercase().as_str() { - "dockerfile" => "dockerfile".to_string(), - "makefile" => "makefile".to_string(), - "cargo.toml" | "pyproject.toml" => "toml".to_string(), - "package.json" => "json".to_string(), - _ => "txt".to_string(), - } - } else { - "txt".to_string() - } + let default_lang = "txt".to_string(); + + let path = match file_path { + Some(p) => p, + None => return default_lang, + }; + + if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) { + match extension.to_lowercase().as_str() { + "rs" => "rs".to_string(), + "py" => "py".to_string(), + "js" => "js".to_string(), + "ts" => "ts".to_string(), + "tsx" => "tsx".to_string(), + "jsx" => "jsx".to_string(), + "c" => "c".to_string(), + "cpp" | "cc" | "cxx" => "cpp".to_string(), + "h" | "hpp" => "cpp".to_string(), + "java" => "java".to_string(), + "go" => "go".to_string(), + "php" => "php".to_string(), + "rb" => "rb".to_string(), + "cs" => "cs".to_string(), + "swift" => "swift".to_string(), + "kt" => "kt".to_string(), + "scala" => "scala".to_string(), + "sh" | "bash" | "zsh" | "fish" => "sh".to_string(), + "html" | "htm" => "html".to_string(), + "xml" => "xml".to_string(), + "css" => "css".to_string(), + "scss" | "sass" => "scss".to_string(), + "json" => "json".to_string(), + "yaml" | "yml" => "yaml".to_string(), + "toml" => "toml".to_string(), + "md" | "markdown" => "md".to_string(), + "sql" => "sql".to_string(), + "lua" => "lua".to_string(), + "vim" => "vim".to_string(), + "dockerfile" => "dockerfile".to_string(), + "makefile" => "makefile".to_string(), + _ => default_lang, + } + } else if let Some(filename) = path.file_name().and_then(|name| name.to_str()) { + match filename.to_lowercase().as_str() { + "dockerfile" => "dockerfile".to_string(), + "makefile" => "makefile".to_string(), + "cargo.toml" | "pyproject.toml" => "toml".to_string(), + "package.json" => "json".to_string(), + _ => default_lang, } } else { - "txt".to_string() + default_lang } } -- 2.47.1 From 5dc0b6d638915a7ae09da384da66bd7f18c98787 Mon Sep 17 00:00:00 2001 From: candle Date: Wed, 23 Jul 2025 13:14:04 -0400 Subject: [PATCH 6/9] file diffs are kept separate --- Cargo.toml | 3 +- README.md | 5 +- src/app/state/config.rs | 1 - src/app/state/find.rs | 2 - src/app/state/state_cache.rs | 102 ++++++++++++++++++++++++++++----- src/app/state/tabs.rs | 3 - src/app/state/ui.rs | 7 --- src/app/theme.rs | 6 -- src/ui/central_panel/editor.rs | 1 - src/ui/preferences_window.rs | 16 +++--- 10 files changed, 102 insertions(+), 44 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 87258b6..7ef5dff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ced" -version = "0.0.9" +version = "0.1.3" edition = "2024" [dependencies] @@ -16,3 +16,4 @@ libc = "0.2.174" syntect = "5.2.0" plist = "1.7.4" diffy = "0.4.2" +uuid = { version = "1.0", features = ["v4"] } diff --git a/README.md b/README.md index 50117b3..e1aa170 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ There is a disturbing lack of simple GUI text editors available on Linux nativel ## Features * Sane text editing with standard keybindings (`Ctrl+A`, `Ctrl+C`, etc.). -* Opens with a blank slate for quick typing, remember Notepad? +* Choose between opening fresh every time, like Notepad, or maintaining a consistent state like Notepad++. * Separate UI zoom that doesn't affect font size (`Ctrl+Shift` + `+`/`-`). * Ricers rejoice, your `pywal` colors will be used! * Weirdly smooth typing experience. @@ -39,6 +39,7 @@ sudo install -Dm644 ced.desktop /usr/share/applications/ced.desktop Here is an example `config.toml`: ```toml +state_cache = true auto_hide_toolbar = false show_line_numbers = false word_wrap = false @@ -53,6 +54,7 @@ syntax_highlighting = true | Option | Default | Description | |--------|---------|-------------| +| `state_cache` | `false` | If `true`, open files will have their unsaved changes cached and will be automatically opened when starting a new session. | | `auto_hide_toolbar` | `false` | If `true`, the menu bar at the top will be hidden. Move your mouse to the top of the window to reveal it. | | `hide_tab_bar` | 'true' | If `false`, a separate tab bar will be drawn below the toolbar. | | `show_line_numbers` | `false` | If `true`, line numbers will be displayed on the side specified by `line_side`. | @@ -67,7 +69,6 @@ syntax_highlighting = true In order of importance. | Feature | Info | | ------- | ---- | -| **State/Cache:** | A toggleable option to keep an application state and prevent "Quit without saving" warnings. | | **LSP:** | Looking at allowing you to use/attach your own tools for this. | | **Choose Font** | More than just Monospace/Proportional. | | **Vim Mode:** | It's in-escapable. | diff --git a/src/app/state/config.rs b/src/app/state/config.rs index adb36fd..6e6ca66 100644 --- a/src/app/state/config.rs +++ b/src/app/state/config.rs @@ -22,7 +22,6 @@ impl TextEditor { pub fn from_config_with_context(config: Config, cc: &eframe::CreationContext<'_>) -> Self { let mut editor = Self::from_config(config); - // Load state cache if enabled if let Err(e) = editor.load_state_cache() { eprintln!("Failed to load state cache: {e}"); } diff --git a/src/app/state/find.rs b/src/app/state/find.rs index bae0f3f..0e30610 100644 --- a/src/app/state/find.rs +++ b/src/app/state/find.rs @@ -30,7 +30,6 @@ impl TextEditor { let search_slice = if search_content.is_char_boundary(start) { &search_content[start..] } else { - // Find next valid boundary while start < search_content.len() && !search_content.is_char_boundary(start) { start += 1; } @@ -45,7 +44,6 @@ impl TextEditor { self.find_matches .push((absolute_pos, absolute_pos + query.len())); - // Advance to next valid character boundary instead of just +1 start = absolute_pos + 1; while start < search_content.len() && !search_content.is_char_boundary(start) { start += 1; diff --git a/src/app/state/state_cache.rs b/src/app/state/state_cache.rs index f19e4f1..afc92fa 100644 --- a/src/app/state/state_cache.rs +++ b/src/app/state/state_cache.rs @@ -2,10 +2,11 @@ use super::editor::TextEditor; use crate::app::tab::{Tab, compute_content_hash}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; +use uuid::Uuid; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CachedTab { - pub diff: Option, + pub diff_file: Option, // Path to diff file for modified tabs pub full_content: Option, // This is used for 'new files' that don't have a path pub file_path: Option, pub is_modified: bool, @@ -20,18 +21,40 @@ pub struct StateCache { pub tab_counter: usize, } +fn create_diff_file(diff_content: &str) -> Result> { + let diffs_dir = TextEditor::diffs_cache_dir().ok_or("Cannot determine cache directory")?; + std::fs::create_dir_all(&diffs_dir)?; + + let diff_filename = format!("{}.diff", Uuid::new_v4()); + let diff_path = diffs_dir.join(diff_filename); + + std::fs::write(&diff_path, diff_content)?; + Ok(diff_path) +} + +fn load_diff_file(diff_path: &PathBuf) -> Result> { + Ok(std::fs::read_to_string(diff_path)?) +} + impl From<&Tab> for CachedTab { fn from(tab: &Tab) -> Self { if let Some(file_path) = &tab.file_path { let original_content = std::fs::read_to_string(file_path).unwrap_or_default(); - let diff = if tab.is_modified { - Some(diffy::create_patch(&original_content, &tab.content).to_string()) + let diff_file = if tab.is_modified { + let diff_content = diffy::create_patch(&original_content, &tab.content); + match create_diff_file(&diff_content.to_string()) { + Ok(path) => Some(path), + Err(e) => { + eprintln!("Warning: Failed to create diff file: {}", e); + None + } + } } else { None }; Self { - diff, + diff_file, full_content: None, file_path: tab.file_path.clone(), is_modified: tab.is_modified, @@ -40,7 +63,7 @@ impl From<&Tab> for CachedTab { } } else { Self { - diff: None, + diff_file: None, full_content: Some(tab.content.clone()), file_path: None, is_modified: tab.is_modified, @@ -55,21 +78,30 @@ impl From for Tab { fn from(cached: CachedTab) -> Self { if let Some(file_path) = cached.file_path { let original_content = std::fs::read_to_string(&file_path).unwrap_or_default(); - let current_content = if let Some(diff_str) = cached.diff { - match diffy::Patch::from_str(&diff_str) { - Ok(patch) => { - match diffy::apply(&original_content, &patch) { - Ok(content) => content, + let current_content = if let Some(diff_path) = cached.diff_file { + match load_diff_file(&diff_path) { + Ok(diff_content) => { + match diffy::Patch::from_str(&diff_content) { + Ok(patch) => { + match diffy::apply(&original_content, &patch) { + Ok(content) => content, + Err(_) => { + eprintln!("Warning: Failed to apply diff for {}, using original content", + file_path.display()); + original_content + } + } + } Err(_) => { - eprintln!("Warning: Failed to apply diff for {}, using original content", + eprintln!("Warning: Failed to parse diff for {}, using original content", file_path.display()); original_content } } } - Err(_) => { - eprintln!("Warning: Failed to parse diff for {}, using original content", - file_path.display()); + Err(e) => { + eprintln!("Warning: Failed to load diff file {:?}: {}, using original content", + diff_path, e); original_content } } @@ -116,6 +148,35 @@ impl TextEditor { Some(cache_dir.join("state.json")) } + pub fn diffs_cache_dir() -> Option { + let cache_dir = if let Some(cache_dir) = dirs::cache_dir() { + cache_dir.join(env!("CARGO_PKG_NAME")) + } else if let Some(home_dir) = dirs::home_dir() { + home_dir.join(".cache").join(env!("CARGO_PKG_NAME")) + } else { + return None; + }; + + Some(cache_dir.join("diffs")) + } + + fn cleanup_orphaned_diffs(active_diff_files: &[PathBuf]) -> Result<(), Box> { + if let Some(diffs_dir) = Self::diffs_cache_dir() { + if diffs_dir.exists() { + for entry in std::fs::read_dir(diffs_dir)? { + let entry = entry?; + let path = entry.path(); + if path.extension().and_then(|s| s.to_str()) == Some("diff") { + if !active_diff_files.contains(&path) { + let _ = std::fs::remove_file(path); + } + } + } + } + } + Ok(()) + } + pub fn load_state_cache(&mut self) -> Result<(), Box> { if !self.state_cache { return Ok(()); @@ -157,6 +218,12 @@ impl TextEditor { tab_counter: self.tab_counter, }; + let active_diff_files: Vec = state_cache.tabs + .iter() + .filter_map(|tab| tab.diff_file.clone()) + .collect(); + let _ = Self::cleanup_orphaned_diffs(&active_diff_files); + let content = serde_json::to_string_pretty(&state_cache)?; std::fs::write(&cache_path, content)?; @@ -169,6 +236,13 @@ impl TextEditor { std::fs::remove_file(cache_path)?; } } + + if let Some(diffs_dir) = Self::diffs_cache_dir() { + if diffs_dir.exists() { + let _ = std::fs::remove_dir_all(diffs_dir); + } + } + Ok(()) } } \ No newline at end of file diff --git a/src/app/state/tabs.rs b/src/app/state/tabs.rs index bfaba5e..b491955 100644 --- a/src/app/state/tabs.rs +++ b/src/app/state/tabs.rs @@ -19,7 +19,6 @@ impl TextEditor { } self.text_needs_processing = true; - // Save state cache after adding new tab if let Err(e) = self.save_state_cache() { eprintln!("Failed to save state cache: {e}"); } @@ -38,7 +37,6 @@ impl TextEditor { } self.text_needs_processing = true; - // Save state cache after closing tab if let Err(e) = self.save_state_cache() { eprintln!("Failed to save state cache: {e}"); } @@ -53,7 +51,6 @@ impl TextEditor { } self.text_needs_processing = true; - // Save state cache after switching tabs if let Err(e) = self.save_state_cache() { eprintln!("Failed to save state cache: {e}"); } diff --git a/src/app/state/ui.rs b/src/app/state/ui.rs index 1c003ac..0571615 100644 --- a/src/app/state/ui.rs +++ b/src/app/state/ui.rs @@ -23,7 +23,6 @@ impl TextEditor { } } - /// Get the configured font ID based on the editor's font settings pub fn get_font_id(&self) -> egui::FontId { let font_family = match self.font_family.as_str() { "Monospace" => egui::FontFamily::Monospace, @@ -32,13 +31,11 @@ impl TextEditor { egui::FontId::new(self.font_size, font_family) } - /// Immediately apply theme and save to configuration pub fn set_theme(&mut self, ctx: &egui::Context) { theme::apply(self.theme, ctx); self.save_config(); } - /// Apply font settings with immediate text reprocessing pub fn apply_font_settings(&mut self, ctx: &egui::Context) { let font_family = match self.font_family.as_str() { "Monospace" => egui::FontFamily::Monospace, @@ -56,21 +53,18 @@ impl TextEditor { self.save_config(); } - /// Apply font settings with immediate text reprocessing pub fn apply_font_settings_with_ui(&mut self, ctx: &egui::Context, ui: &egui::Ui) { self.apply_font_settings(ctx); self.reprocess_text_for_font_change(ui); self.font_settings_changed = false; } - /// Trigger immediate text reprocessing when font settings change pub fn reprocess_text_for_font_change(&mut self, ui: &egui::Ui) { if let Some(active_tab) = self.get_active_tab() { self.process_text_for_rendering(&active_tab.content.to_string(), ui); } } - /// Calculates the available width for the text editor, accounting for line numbers and separator pub fn calculate_editor_dimensions(&self, ui: &egui::Ui) -> EditorDimensions { let total_available_width = ui.available_width(); @@ -114,7 +108,6 @@ impl TextEditor { } } - /// Calculate the available width for non-word-wrapped content based on processed text data pub fn calculate_content_based_width(&self, ui: &egui::Ui) -> f32 { let processing_result = self.get_text_processing_result(); diff --git a/src/app/theme.rs b/src/app/theme.rs index 0a6dac4..afce1c2 100644 --- a/src/app/theme.rs +++ b/src/app/theme.rs @@ -261,7 +261,6 @@ fn build_custom_theme_plist( let mut settings_array = Vec::new(); - // Global settings let mut global_settings_dict = Dictionary::new(); let mut inner_global_settings = Dictionary::new(); inner_global_settings.insert("background".to_string(), Value::String(background_color.to_string())); @@ -269,7 +268,6 @@ fn build_custom_theme_plist( global_settings_dict.insert("settings".to_string(), Value::Dictionary(inner_global_settings)); settings_array.push(Value::Dictionary(global_settings_dict)); - // Comment scope let mut comment_scope_dict = Dictionary::new(); comment_scope_dict.insert("name".to_string(), Value::String("Comment".to_string())); comment_scope_dict.insert("scope".to_string(), Value::String("comment".to_string())); @@ -279,7 +277,6 @@ fn build_custom_theme_plist( comment_scope_dict.insert("settings".to_string(), Value::Dictionary(comment_settings)); settings_array.push(Value::Dictionary(comment_scope_dict)); - // String scope let mut string_scope_dict = Dictionary::new(); string_scope_dict.insert("name".to_string(), Value::String("String".to_string())); string_scope_dict.insert("scope".to_string(), Value::String("string".to_string())); @@ -288,7 +285,6 @@ fn build_custom_theme_plist( string_scope_dict.insert("settings".to_string(), Value::Dictionary(string_settings)); settings_array.push(Value::Dictionary(string_scope_dict)); - // Keyword scope let mut keyword_scope_dict = Dictionary::new(); keyword_scope_dict.insert("name".to_string(), Value::String("Keyword".to_string())); keyword_scope_dict.insert("scope".to_string(), Value::String("keyword".to_string())); @@ -297,8 +293,6 @@ fn build_custom_theme_plist( keyword_scope_dict.insert("settings".to_string(), Value::Dictionary(keyword_settings)); settings_array.push(Value::Dictionary(keyword_scope_dict)); - // Add more scopes as needed... - root_dict.insert("settings".to_string(), Value::Array(settings_array)); Value::Dictionary(root_dict) diff --git a/src/ui/central_panel/editor.rs b/src/ui/central_panel/editor.rs index 15ba191..1c0311e 100644 --- a/src/ui/central_panel/editor.rs +++ b/src/ui/central_panel/editor.rs @@ -158,7 +158,6 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R None }; - // Save state cache when content changes (after releasing the borrow) if content_changed { if let Err(e) = app.save_state_cache() { eprintln!("Failed to save state cache: {e}"); diff --git a/src/ui/preferences_window.rs b/src/ui/preferences_window.rs index 9576d82..b861684 100644 --- a/src/ui/preferences_window.rs +++ b/src/ui/preferences_window.rs @@ -32,9 +32,9 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) { ui.horizontal(|ui| { if ui - .checkbox(&mut app.state_cache, "State Cache") + .checkbox(&mut app.state_cache, "Cache State") .on_hover_text( - "Save and restore open tabs and unsaved changes between sessions" + "Unsaved changes will be cached between sessions" ) .changed() { @@ -56,6 +56,12 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) { { app.save_config(); } + if ui + .checkbox(&mut app.syntax_highlighting, "Syntax Highlighting") + .changed() + { + app.save_config(); + } }); ui.add_space(4.0); @@ -79,11 +85,6 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) { { app.save_config(); } - }); - - ui.add_space(4.0); - - ui.horizontal(|ui| { if ui .checkbox(&mut app.hide_tab_bar, "Hide Tab Bar") .on_hover_text("Hide the tab bar and show tab title in menu bar instead") @@ -91,6 +92,7 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) { { app.save_config(); } + }); ui.add_space(12.0); -- 2.47.1 From e5b1214f631f70434f069abf263c20a90ed981e8 Mon Sep 17 00:00:00 2001 From: candle Date: Wed, 23 Jul 2025 13:17:10 -0400 Subject: [PATCH 7/9] formatting fixes --- src/app/state/config.rs | 4 +-- src/app/state/state_cache.rs | 63 ++++++++++++++++++++---------------- src/app/state/tabs.rs | 6 ++-- src/app/theme.rs | 48 ++++++++++++++++++++------- src/io.rs | 4 +-- src/main.rs | 2 +- src/ui/central_panel.rs | 2 +- src/ui/menu_bar.rs | 5 ++- src/ui/preferences_window.rs | 11 ++----- src/ui/tab_bar.rs | 12 +++++-- 10 files changed, 96 insertions(+), 61 deletions(-) diff --git a/src/app/state/config.rs b/src/app/state/config.rs index 6e6ca66..1613936 100644 --- a/src/app/state/config.rs +++ b/src/app/state/config.rs @@ -21,11 +21,11 @@ impl TextEditor { pub fn from_config_with_context(config: Config, cc: &eframe::CreationContext<'_>) -> Self { let mut editor = Self::from_config(config); - + if let Err(e) = editor.load_state_cache() { eprintln!("Failed to load state cache: {e}"); } - + theme::apply(editor.theme, &cc.egui_ctx); cc.egui_ctx.options_mut(|o| o.zoom_with_keyboard = false); diff --git a/src/app/state/state_cache.rs b/src/app/state/state_cache.rs index afc92fa..5912fa1 100644 --- a/src/app/state/state_cache.rs +++ b/src/app/state/state_cache.rs @@ -1,5 +1,5 @@ use super::editor::TextEditor; -use crate::app::tab::{Tab, compute_content_hash}; +use crate::app::tab::{compute_content_hash, Tab}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use uuid::Uuid; @@ -24,10 +24,10 @@ pub struct StateCache { fn create_diff_file(diff_content: &str) -> Result> { let diffs_dir = TextEditor::diffs_cache_dir().ok_or("Cannot determine cache directory")?; std::fs::create_dir_all(&diffs_dir)?; - + let diff_filename = format!("{}.diff", Uuid::new_v4()); let diff_path = diffs_dir.join(diff_filename); - + std::fs::write(&diff_path, diff_content)?; Ok(diff_path) } @@ -52,7 +52,7 @@ impl From<&Tab> for CachedTab { } else { None }; - + Self { diff_file, full_content: None, @@ -82,46 +82,49 @@ impl From for Tab { match load_diff_file(&diff_path) { Ok(diff_content) => { match diffy::Patch::from_str(&diff_content) { - Ok(patch) => { - match diffy::apply(&original_content, &patch) { - Ok(content) => content, - Err(_) => { - eprintln!("Warning: Failed to apply diff for {}, using original content", + Ok(patch) => match diffy::apply(&original_content, &patch) { + Ok(content) => content, + Err(_) => { + eprintln!("Warning: Failed to apply diff for {}, using original content", file_path.display()); - original_content - } + original_content } - } + }, Err(_) => { - eprintln!("Warning: Failed to parse diff for {}, using original content", - file_path.display()); + eprintln!( + "Warning: Failed to parse diff for {}, using original content", + file_path.display() + ); original_content } } } Err(e) => { - eprintln!("Warning: Failed to load diff file {:?}: {}, using original content", - diff_path, e); + eprintln!( + "Warning: Failed to load diff file {:?}: {}, using original content", + diff_path, e + ); original_content } } } else { original_content }; - - let original_hash = compute_content_hash(&std::fs::read_to_string(&file_path).unwrap_or_default()); + + let original_hash = + compute_content_hash(&std::fs::read_to_string(&file_path).unwrap_or_default()); let expected_hash = cached.original_content_hash; - + let mut tab = Tab::new_with_file(current_content, file_path); tab.title = cached.title; - + if original_hash != expected_hash { tab.is_modified = true; } else { tab.is_modified = cached.is_modified; tab.original_content_hash = cached.original_content_hash; } - + tab } else { let content = cached.full_content.unwrap_or_default(); @@ -160,7 +163,9 @@ impl TextEditor { Some(cache_dir.join("diffs")) } - fn cleanup_orphaned_diffs(active_diff_files: &[PathBuf]) -> Result<(), Box> { + fn cleanup_orphaned_diffs( + active_diff_files: &[PathBuf], + ) -> Result<(), Box> { if let Some(diffs_dir) = Self::diffs_cache_dir() { if diffs_dir.exists() { for entry in std::fs::read_dir(diffs_dir)? { @@ -183,7 +188,7 @@ impl TextEditor { } let cache_path = Self::state_cache_path().ok_or("Cannot determine cache directory")?; - + if !cache_path.exists() { return Ok(()); } @@ -193,7 +198,8 @@ impl TextEditor { if !state_cache.tabs.is_empty() { self.tabs = state_cache.tabs.into_iter().map(Tab::from).collect(); - self.active_tab_index = std::cmp::min(state_cache.active_tab_index, self.tabs.len() - 1); + self.active_tab_index = + std::cmp::min(state_cache.active_tab_index, self.tabs.len() - 1); self.tab_counter = state_cache.tab_counter; self.text_needs_processing = true; } @@ -218,7 +224,8 @@ impl TextEditor { tab_counter: self.tab_counter, }; - let active_diff_files: Vec = state_cache.tabs + let active_diff_files: Vec = state_cache + .tabs .iter() .filter_map(|tab| tab.diff_file.clone()) .collect(); @@ -236,13 +243,13 @@ impl TextEditor { std::fs::remove_file(cache_path)?; } } - + if let Some(diffs_dir) = Self::diffs_cache_dir() { if diffs_dir.exists() { let _ = std::fs::remove_dir_all(diffs_dir); } } - + Ok(()) } -} \ No newline at end of file +} diff --git a/src/app/state/tabs.rs b/src/app/state/tabs.rs index b491955..21e2e30 100644 --- a/src/app/state/tabs.rs +++ b/src/app/state/tabs.rs @@ -18,7 +18,7 @@ impl TextEditor { self.update_find_matches(); } self.text_needs_processing = true; - + if let Err(e) = self.save_state_cache() { eprintln!("Failed to save state cache: {e}"); } @@ -36,7 +36,7 @@ impl TextEditor { self.update_find_matches(); } self.text_needs_processing = true; - + if let Err(e) = self.save_state_cache() { eprintln!("Failed to save state cache: {e}"); } @@ -50,7 +50,7 @@ impl TextEditor { self.update_find_matches(); } self.text_needs_processing = true; - + if let Err(e) = self.save_state_cache() { eprintln!("Failed to save state cache: {e}"); } diff --git a/src/app/theme.rs b/src/app/theme.rs index afce1c2..7b1b271 100644 --- a/src/app/theme.rs +++ b/src/app/theme.rs @@ -1,7 +1,9 @@ use eframe::egui; use plist::{Dictionary, Value}; use std::collections::BTreeMap; -use syntect::highlighting::{Theme as SyntectTheme, ThemeSet, ThemeSettings, Color as SyntectColor, UnderlineOption}; +use syntect::highlighting::{ + Color as SyntectColor, Theme as SyntectTheme, ThemeSet, ThemeSettings, UnderlineOption, +}; #[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize, Default)] pub enum Theme { @@ -233,11 +235,18 @@ pub fn create_code_theme_from_visuals(visuals: &egui::Visuals, font_size: f32) - blend_colors(egui::Color32::from_rgb(128, 0, 128), text_color, 0.8) // Purple-like }; - let plist_theme = build_custom_theme_plist("System", &format!("{:?}", bg_color), &format!("{:?}", text_color), &format!("{:?}", comment_color), &format!("{:?}", string_color), &format!("{:?}", keyword_color)); + let plist_theme = build_custom_theme_plist( + "System", + &format!("{:?}", bg_color), + &format!("{:?}", text_color), + &format!("{:?}", comment_color), + &format!("{:?}", string_color), + &format!("{:?}", keyword_color), + ); let file = std::fs::File::create("system.tmTheme").unwrap(); let writer = std::io::BufWriter::new(file); - let _ =plist::to_writer_xml(writer, &plist_theme); + let _ = plist::to_writer_xml(writer, &plist_theme); let loaded_file = std::fs::File::open("system.tmTheme").unwrap(); let mut loaded_reader = std::io::BufReader::new(loaded_file); @@ -245,7 +254,6 @@ pub fn create_code_theme_from_visuals(visuals: &egui::Visuals, font_size: f32) - let mut set = ThemeSet::new(); set.add_from_folder(".").unwrap(); return set; - } fn build_custom_theme_plist( @@ -263,16 +271,28 @@ fn build_custom_theme_plist( let mut global_settings_dict = Dictionary::new(); let mut inner_global_settings = Dictionary::new(); - inner_global_settings.insert("background".to_string(), Value::String(background_color.to_string())); - inner_global_settings.insert("foreground".to_string(), Value::String(foreground_color.to_string())); - global_settings_dict.insert("settings".to_string(), Value::Dictionary(inner_global_settings)); + inner_global_settings.insert( + "background".to_string(), + Value::String(background_color.to_string()), + ); + inner_global_settings.insert( + "foreground".to_string(), + Value::String(foreground_color.to_string()), + ); + global_settings_dict.insert( + "settings".to_string(), + Value::Dictionary(inner_global_settings), + ); settings_array.push(Value::Dictionary(global_settings_dict)); let mut comment_scope_dict = Dictionary::new(); comment_scope_dict.insert("name".to_string(), Value::String("Comment".to_string())); comment_scope_dict.insert("scope".to_string(), Value::String("comment".to_string())); let mut comment_settings = Dictionary::new(); - comment_settings.insert("foreground".to_string(), Value::String(comment_color.to_string())); + comment_settings.insert( + "foreground".to_string(), + Value::String(comment_color.to_string()), + ); comment_settings.insert("fontStyle".to_string(), Value::String("italic".to_string())); comment_scope_dict.insert("settings".to_string(), Value::Dictionary(comment_settings)); settings_array.push(Value::Dictionary(comment_scope_dict)); @@ -281,7 +301,10 @@ fn build_custom_theme_plist( string_scope_dict.insert("name".to_string(), Value::String("String".to_string())); string_scope_dict.insert("scope".to_string(), Value::String("string".to_string())); let mut string_settings = Dictionary::new(); - string_settings.insert("foreground".to_string(), Value::String(string_color.to_string())); + string_settings.insert( + "foreground".to_string(), + Value::String(string_color.to_string()), + ); string_scope_dict.insert("settings".to_string(), Value::Dictionary(string_settings)); settings_array.push(Value::Dictionary(string_scope_dict)); @@ -289,11 +312,14 @@ fn build_custom_theme_plist( keyword_scope_dict.insert("name".to_string(), Value::String("Keyword".to_string())); keyword_scope_dict.insert("scope".to_string(), Value::String("keyword".to_string())); let mut keyword_settings = Dictionary::new(); - keyword_settings.insert("foreground".to_string(), Value::String(keyword_color.to_string())); + keyword_settings.insert( + "foreground".to_string(), + Value::String(keyword_color.to_string()), + ); keyword_scope_dict.insert("settings".to_string(), Value::Dictionary(keyword_settings)); settings_array.push(Value::Dictionary(keyword_scope_dict)); root_dict.insert("settings".to_string(), Value::Array(settings_array)); Value::Dictionary(root_dict) -} \ No newline at end of file +} diff --git a/src/io.rs b/src/io.rs index d2ec674..7ee8e1c 100644 --- a/src/io.rs +++ b/src/io.rs @@ -43,7 +43,7 @@ pub(crate) fn open_file(app: &mut TextEditor) { if app.show_find && !app.find_query.is_empty() { app.update_find_matches(); } - + if let Err(e) = app.save_state_cache() { eprintln!("Failed to save state cache: {e}"); } @@ -85,7 +85,7 @@ pub(crate) fn save_to_path(app: &mut TextEditor, path: PathBuf) { active_tab.file_path = Some(path.to_path_buf()); active_tab.title = title.to_string(); active_tab.mark_as_saved(); - + if let Err(e) = app.save_state_cache() { eprintln!("Failed to save state cache: {e}"); } diff --git a/src/main.rs b/src/main.rs index 0a0716a..46402b3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use std::io::IsTerminal; mod app; mod io; mod ui; -use app::{TextEditor, config::Config}; +use app::{config::Config, TextEditor}; fn main() -> eframe::Result { let _args: Vec = env::args().collect(); diff --git a/src/ui/central_panel.rs b/src/ui/central_panel.rs index 6f6e7e6..efb1e0a 100644 --- a/src/ui/central_panel.rs +++ b/src/ui/central_panel.rs @@ -1,7 +1,7 @@ mod editor; mod find_highlight; -mod line_numbers; mod languages; +mod line_numbers; use crate::app::TextEditor; use eframe::egui; diff --git a/src/ui/menu_bar.rs b/src/ui/menu_bar.rs index a7ff6cc..d37da87 100644 --- a/src/ui/menu_bar.rs +++ b/src/ui/menu_bar.rs @@ -185,7 +185,10 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) { app.save_config(); ui.close_kind(UiKind::Menu); } - if ui.checkbox(&mut app.syntax_highlighting, "Syntax Highlighting").clicked() { + if ui + .checkbox(&mut app.syntax_highlighting, "Syntax Highlighting") + .clicked() + { app.save_config(); ui.close_kind(UiKind::Menu); } diff --git a/src/ui/preferences_window.rs b/src/ui/preferences_window.rs index b861684..54af9ce 100644 --- a/src/ui/preferences_window.rs +++ b/src/ui/preferences_window.rs @@ -26,16 +26,13 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) { }) .show(ctx, |ui| { ui.vertical_centered(|ui| { - ui.heading("General Settings"); ui.add_space(8.0); ui.horizontal(|ui| { if ui .checkbox(&mut app.state_cache, "Cache State") - .on_hover_text( - "Unsaved changes will be cached between sessions" - ) + .on_hover_text("Unsaved changes will be cached between sessions") .changed() { app.save_config(); @@ -67,10 +64,7 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) { ui.add_space(4.0); ui.horizontal(|ui| { - if ui - .checkbox(&mut app.word_wrap, "Word Wrap") - .changed() - { + if ui.checkbox(&mut app.word_wrap, "Word Wrap").changed() { app.save_config(); } }); @@ -92,7 +86,6 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) { { app.save_config(); } - }); ui.add_space(12.0); diff --git a/src/ui/tab_bar.rs b/src/ui/tab_bar.rs index 86f631b..84f9583 100644 --- a/src/ui/tab_bar.rs +++ b/src/ui/tab_bar.rs @@ -39,7 +39,11 @@ pub(crate) fn tab_bar(app: &mut TextEditor, ctx: &egui::Context) { label_text = label_text.italics(); } - let tab_response = ui.add(egui::Label::new(label_text).selectable(false).sense(egui::Sense::click())); + let tab_response = ui.add( + egui::Label::new(label_text) + .selectable(false) + .sense(egui::Sense::click()), + ); if tab_response.clicked() { tab_to_switch = Some(i); } @@ -49,7 +53,10 @@ pub(crate) fn tab_bar(app: &mut TextEditor, ctx: &egui::Context) { let close_button = egui::Button::new("×") .small() .fill(visuals.panel_fill) - .stroke(egui::Stroke::new(0.0, egui::Color32::from_rgb(0, 0, 0))); + .stroke(egui::Stroke::new( + 0.0, + egui::Color32::from_rgb(0, 0, 0), + )); let close_response = ui.add(close_button); if close_response.clicked() { if *is_modified { @@ -90,5 +97,4 @@ pub(crate) fn tab_bar(app: &mut TextEditor, ctx: &egui::Context) { }); app.tab_bar_rect = Some(tab_bar.response.rect); - } -- 2.47.1 From 0c7ae2d1b1b6551a3e1a96507643d1ef8ab9b282 Mon Sep 17 00:00:00 2001 From: candle Date: Sat, 26 Jul 2025 11:50:48 -0400 Subject: [PATCH 8/9] better state caching, started building custom syntax theme --- Cargo.toml | 2 +- src/app/state/config.rs | 35 ++++- src/app/state/state_cache.rs | 2 +- src/app/state/ui.rs | 12 -- src/io.rs | 154 +++++++++++++++++++ src/main.rs | 15 +- src/ui.rs | 1 + src/ui/about_window.rs | 9 +- src/ui/central_panel.rs | 5 +- src/ui/central_panel/editor.rs | 2 +- src/ui/constants.rs | 26 ++++ src/ui/find_window.rs | 15 +- src/ui/preferences_window.rs | 272 +++++++++++++++++---------------- src/ui/shortcuts_window.rs | 69 +++++---- 14 files changed, 419 insertions(+), 200 deletions(-) create mode 100644 src/ui/constants.rs diff --git a/Cargo.toml b/Cargo.toml index 7ef5dff..63ab481 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ eframe = "0.32" egui = "0.32" egui_extras = { version = "0.32", features = ["syntect"] } serde = { version = "1.0.219", features = ["derive"] } -serde_json = "1.0" +serde_json = "1.0.141" rfd = "0.15.4" toml = "0.9.2" dirs = "6.0" diff --git a/src/app/state/config.rs b/src/app/state/config.rs index 1613936..fad9e93 100644 --- a/src/app/state/config.rs +++ b/src/app/state/config.rs @@ -1,6 +1,8 @@ use super::editor::TextEditor; use crate::app::config::Config; use crate::app::theme; +use crate::io; +use std::path::PathBuf; impl TextEditor { pub fn from_config(config: Config) -> Self { @@ -19,13 +21,44 @@ impl TextEditor { } } - pub fn from_config_with_context(config: Config, cc: &eframe::CreationContext<'_>) -> Self { + pub fn from_config_with_context( + config: Config, + cc: &eframe::CreationContext<'_>, + initial_paths: Vec, + ) -> Self { let mut editor = Self::from_config(config); if let Err(e) = editor.load_state_cache() { eprintln!("Failed to load state cache: {e}"); } + if !initial_paths.is_empty() { + let mut opened_any = false; + + for path in initial_paths { + if path.is_file() { + match io::open_file_from_path(&mut editor, path.clone()) { + Ok(()) => opened_any = true, + Err(e) => eprintln!("Error opening file {}: {}", path.display(), e), + } + } else if path.is_dir() { + match io::open_files_from_directory(&mut editor, path.clone()) { + Ok(count) => { + opened_any = true; + println!("Opened {} files from directory {}", count, path.display()); + } + Err(e) => eprintln!("Error opening directory {}: {}", path.display(), e), + } + } else { + eprintln!("Warning: Path does not exist: {}", path.display()); + } + } + + if opened_any { + editor.active_tab_index = editor.tabs.len().saturating_sub(1); + } + } + theme::apply(editor.theme, &cc.egui_ctx); cc.egui_ctx.options_mut(|o| o.zoom_with_keyboard = false); diff --git a/src/app/state/state_cache.rs b/src/app/state/state_cache.rs index 5912fa1..4cda33d 100644 --- a/src/app/state/state_cache.rs +++ b/src/app/state/state_cache.rs @@ -6,7 +6,7 @@ use uuid::Uuid; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CachedTab { - pub diff_file: Option, // Path to diff file for modified tabs + pub diff_file: Option, pub full_content: Option, // This is used for 'new files' that don't have a path pub file_path: Option, pub is_modified: bool, diff --git a/src/app/state/ui.rs b/src/app/state/ui.rs index 0571615..c4f9a6b 100644 --- a/src/app/state/ui.rs +++ b/src/app/state/ui.rs @@ -53,18 +53,6 @@ impl TextEditor { self.save_config(); } - pub fn apply_font_settings_with_ui(&mut self, ctx: &egui::Context, ui: &egui::Ui) { - self.apply_font_settings(ctx); - self.reprocess_text_for_font_change(ui); - self.font_settings_changed = false; - } - - pub fn reprocess_text_for_font_change(&mut self, ui: &egui::Ui) { - if let Some(active_tab) = self.get_active_tab() { - self.process_text_for_rendering(&active_tab.content.to_string(), ui); - } - } - pub fn calculate_editor_dimensions(&self, ui: &egui::Ui) -> EditorDimensions { let total_available_width = ui.available_width(); diff --git a/src/io.rs b/src/io.rs index 7ee8e1c..ac0dd74 100644 --- a/src/io.rs +++ b/src/io.rs @@ -7,6 +7,116 @@ pub(crate) fn new_file(app: &mut TextEditor) { app.add_new_tab(); } +fn is_text_file(path: &PathBuf) -> bool { + if let Some(extension) = path.extension().and_then(|s| s.to_str()) { + matches!( + extension.to_lowercase().as_str(), + "txt" + | "md" + | "markdown" + | "rs" + | "py" + | "js" + | "ts" + | "tsx" + | "jsx" + | "c" + | "cpp" + | "cc" + | "cxx" + | "h" + | "hpp" + | "java" + | "go" + | "php" + | "rb" + | "cs" + | "swift" + | "kt" + | "scala" + | "sh" + | "bash" + | "zsh" + | "fish" + | "html" + | "htm" + | "xml" + | "css" + | "scss" + | "sass" + | "json" + | "yaml" + | "yml" + | "toml" + | "sql" + | "lua" + | "vim" + | "dockerfile" + | "makefile" + | "gitignore" + | "conf" + | "cfg" + | "ini" + | "log" + | "csv" + | "tsv" + ) + } else { + // Files without extensions might be text files, but let's be conservative + // and only include them if they're small and readable + if let Ok(metadata) = fs::metadata(path) { + metadata.len() < 1024 * 1024 // Only consider files smaller than 1MB + } else { + false + } + } +} + +pub(crate) fn open_files_from_directory( + app: &mut TextEditor, + dir_path: PathBuf, +) -> Result { + if !dir_path.is_dir() { + return Err(format!("{} is not a directory", dir_path.display())); + } + + let entries = fs::read_dir(&dir_path) + .map_err(|e| format!("Failed to read directory {}: {}", dir_path.display(), e))?; + + let mut opened_count = 0; + let mut text_files: Vec = Vec::new(); + + // Collect all text files in the directory + for entry in entries { + let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?; + let path = entry.path(); + + if path.is_file() && is_text_file(&path) { + text_files.push(path); + } + } + + // Sort files by name for consistent ordering + text_files.sort(); + + // Open each text file + for file_path in text_files { + match open_file_from_path(app, file_path.clone()) { + Ok(()) => opened_count += 1, + Err(e) => eprintln!("Warning: {}", e), + } + } + + if opened_count == 0 { + Err(format!( + "No text files found in directory {}", + dir_path.display() + )) + } else { + Ok(opened_count) + } +} + pub(crate) fn open_file(app: &mut TextEditor) { if let Some(path) = rfd::FileDialog::new() .add_filter("Text files", &["*"]) @@ -55,6 +165,50 @@ pub(crate) fn open_file(app: &mut TextEditor) { } } +pub(crate) fn open_file_from_path(app: &mut TextEditor, path: PathBuf) -> Result<(), String> { + match fs::read_to_string(&path) { + Ok(content) => { + let should_replace_current_tab = if let Some(active_tab) = app.get_active_tab() { + active_tab.file_path.is_none() + && active_tab.content.is_empty() + && !active_tab.is_modified + } else { + false + }; + + if should_replace_current_tab { + if let Some(active_tab) = app.get_active_tab_mut() { + let title = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("Untitled"); + active_tab.content = content; + active_tab.file_path = Some(path.to_path_buf()); + active_tab.title = title.to_string(); + active_tab.mark_as_saved(); + } + app.text_needs_processing = true; + } else { + let new_tab = Tab::new_with_file(content, path); + app.tabs.push(new_tab); + app.active_tab_index = app.tabs.len() - 1; + app.text_needs_processing = true; + } + + if app.show_find && !app.find_query.is_empty() { + app.update_find_matches(); + } + + if let Err(e) = app.save_state_cache() { + eprintln!("Failed to save state cache: {e}"); + } + + Ok(()) + } + Err(err) => Err(format!("Failed to open file {}: {}", path.display(), err)), + } +} + pub(crate) fn save_file(app: &mut TextEditor) { if let Some(active_tab) = app.get_active_tab() { if let Some(path) = &active_tab.file_path { diff --git a/src/main.rs b/src/main.rs index 46402b3..de06732 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use eframe::egui; use std::env; use std::io::IsTerminal; +use std::path::PathBuf; mod app; mod io; @@ -10,10 +11,12 @@ mod ui; use app::{config::Config, TextEditor}; fn main() -> eframe::Result { - let _args: Vec = env::args().collect(); + let args: Vec = env::args().collect(); + + let initial_paths: Vec = args.iter().skip(1).map(|arg| PathBuf::from(arg)).collect(); + if std::io::stdin().is_terminal() { println!("This is a GUI application, are you sure you want to launch from terminal?"); - // return Ok(()); } let options = eframe::NativeOptions { @@ -29,6 +32,12 @@ fn main() -> eframe::Result { eframe::run_native( "ced", options, - Box::new(move |cc| Ok(Box::new(TextEditor::from_config_with_context(config, cc)))), + Box::new(move |cc| { + Ok(Box::new(TextEditor::from_config_with_context( + config, + cc, + initial_paths, + ))) + }), ) } diff --git a/src/ui.rs b/src/ui.rs index 35cb22e..dd29176 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,5 +1,6 @@ pub(crate) mod about_window; pub(crate) mod central_panel; +pub(crate) mod constants; pub(crate) mod find_window; pub(crate) mod menu_bar; pub(crate) mod preferences_window; diff --git a/src/ui/about_window.rs b/src/ui/about_window.rs index 18f2008..5c4f40a 100644 --- a/src/ui/about_window.rs +++ b/src/ui/about_window.rs @@ -1,4 +1,5 @@ use crate::app::TextEditor; +use crate::ui::constants::*; use eframe::egui; pub(crate) fn about_window(app: &mut TextEditor, ctx: &egui::Context) { @@ -16,20 +17,20 @@ pub(crate) fn about_window(app: &mut TextEditor, ctx: &egui::Context) { .frame(egui::Frame { fill: visuals.window_fill, stroke: visuals.window_stroke, - corner_radius: egui::CornerRadius::same(8), + corner_radius: egui::CornerRadius::same(CORNER_RADIUS), shadow: visuals.window_shadow, - inner_margin: egui::Margin::same(16), + inner_margin: egui::Margin::same(INNER_MARGIN), outer_margin: egui::Margin::same(0), }) .show(ctx, |ui| { ui.vertical_centered(|ui| { ui.label( egui::RichText::new("A stupidly simple, responsive text editor.") - .size(14.0) + .size(UI_TEXT_SIZE) .weak(), ); - ui.add_space(12.0); + ui.add_space(LARGE); let visuals = ui.visuals(); let close_button = egui::Button::new("Close") .fill(visuals.widgets.inactive.bg_fill) diff --git a/src/ui/central_panel.rs b/src/ui/central_panel.rs index efb1e0a..187874c 100644 --- a/src/ui/central_panel.rs +++ b/src/ui/central_panel.rs @@ -4,6 +4,7 @@ mod languages; mod line_numbers; use crate::app::TextEditor; +use crate::ui::constants::*; use eframe::egui; use egui::UiKind; @@ -74,13 +75,13 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) { }; let separator_widget = |ui: &mut egui::Ui| { - ui.add_space(3.0); + ui.add_space(SMALL); let separator_x = ui.cursor().left(); let mut y_range = ui.available_rect_before_wrap().y_range(); y_range.max += 2.0 * font_size; ui.painter() .vline(separator_x, y_range, ui.visuals().window_stroke); - ui.add_space(4.0); + ui.add_space(SMALL); }; egui::ScrollArea::vertical() diff --git a/src/ui/central_panel/editor.rs b/src/ui/central_panel/editor.rs index 1c0311e..26784ed 100644 --- a/src/ui/central_panel/editor.rs +++ b/src/ui/central_panel/editor.rs @@ -103,8 +103,8 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R let mut layouter = |ui: &egui::Ui, string: &dyn egui::TextBuffer, wrap_width: f32| { // let syntect_theme = // crate::app::theme::create_code_theme_from_visuals(ui.visuals(), font_size); + let theme = egui_extras::syntax_highlighting::CodeTheme::from_style(ui.style()); let text = string.as_str(); - let theme = egui_extras::syntax_highlighting::CodeTheme::dark(font_size); let mut layout_job = if syntax_highlighting_enabled && language != "txt" { // let mut settings = egui_extras::syntax_highlighting::SyntectSettings::default(); // settings.ts = syntect_theme; diff --git a/src/ui/constants.rs b/src/ui/constants.rs new file mode 100644 index 0000000..c8dd319 --- /dev/null +++ b/src/ui/constants.rs @@ -0,0 +1,26 @@ +pub const SMALL: f32 = 4.0; +pub const MEDIUM: f32 = 8.0; +pub const LARGE: f32 = 12.0; +pub const VLARGE: f32 = 16.0; + +pub const UI_HEADER_SIZE: f32 = 18.0; +pub const UI_TEXT_SIZE: f32 = 14.0; + +pub const MIN_FONT_SIZE: f32 = 8.0; +pub const MAX_FONT_SIZE: f32 = 32.0; + +pub const WINDOW_WIDTH_RATIO: f32 = 0.6; +pub const WINDOW_HEIGHT_RATIO: f32 = 0.7; +pub const WINDOW_MIN_WIDTH: f32 = 300.0; +pub const WINDOW_MAX_WIDTH: f32 = 400.0; +pub const WINDOW_MIN_HEIGHT: f32 = 250.0; +pub const WINDOW_MAX_HEIGHT: f32 = 500.0; + +pub const CORNER_RADIUS: u8 = 8; + +pub const FONT_SIZE_INPUT_WIDTH: f32 = 24.0; +pub const DEFAULT_FONT_SIZE_STR: &str = "14"; + +pub const PREVIEW_AREA_MAX_HEIGHT: f32 = 150.0; + +pub const INNER_MARGIN: i8 = 8; \ No newline at end of file diff --git a/src/ui/find_window.rs b/src/ui/find_window.rs index 7fed489..446e5cf 100644 --- a/src/ui/find_window.rs +++ b/src/ui/find_window.rs @@ -1,4 +1,5 @@ use crate::app::TextEditor; +use crate::ui::constants::*; use eframe::egui; pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) { @@ -37,9 +38,9 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) { .frame(egui::Frame { fill: visuals.window_fill, stroke: visuals.window_stroke, - corner_radius: egui::CornerRadius::same(8), + corner_radius: egui::CornerRadius::same(CORNER_RADIUS), shadow: visuals.window_shadow, - inner_margin: egui::Margin::same(16), + inner_margin: egui::Margin::same(INNER_MARGIN), outer_margin: egui::Margin::same(0), }) .show(ctx, |ui| { @@ -78,7 +79,7 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) { if app.show_replace_section { ui.horizontal(|ui| { - ui.add_space(4.0); + ui.add_space(SMALL); ui.label("Replace:"); let _replace_response = ui.add( egui::TextEdit::singleline(&mut app.replace_query) @@ -88,7 +89,7 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) { }); } - ui.add_space(8.0); + ui.add_space(MEDIUM); ui.horizontal(|ui| { let case_sensitive_changed = ui @@ -98,7 +99,7 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) { query_changed = true; } if app.show_replace_section { - ui.add_space(8.0); + ui.add_space(MEDIUM); let replace_current_enabled = !app.find_matches.is_empty() && app.current_match_index.is_some(); @@ -117,7 +118,7 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) { } }); - ui.add_space(8.0); + ui.add_space(MEDIUM); ui.horizontal(|ui| { let match_text = if app.find_matches.is_empty() { @@ -139,7 +140,7 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) { should_close = true; } - ui.add_space(4.0); + ui.add_space(SMALL); let next_enabled = !app.find_matches.is_empty(); ui.add_enabled_ui(next_enabled, |ui| { diff --git a/src/ui/preferences_window.rs b/src/ui/preferences_window.rs index 54af9ce..b2f94d6 100644 --- a/src/ui/preferences_window.rs +++ b/src/ui/preferences_window.rs @@ -1,11 +1,14 @@ use crate::app::TextEditor; +use crate::ui::constants::*; use eframe::egui; pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) { let visuals = &ctx.style().visuals; let screen_rect = ctx.screen_rect(); - let window_width = (screen_rect.width() * 0.6).clamp(300.0, 400.0); - let window_height = (screen_rect.height() * 0.7).clamp(250.0, 500.0); + let window_width = (screen_rect.width() * WINDOW_WIDTH_RATIO) + .clamp(WINDOW_MIN_WIDTH, WINDOW_MAX_WIDTH); + let window_height = (screen_rect.height() * WINDOW_HEIGHT_RATIO) + .clamp(WINDOW_MIN_HEIGHT, WINDOW_MAX_HEIGHT); let max_size = egui::Vec2::new(window_width, window_height); egui::Window::new("Preferences") @@ -19,169 +22,168 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) { .frame(egui::Frame { fill: visuals.window_fill, stroke: visuals.window_stroke, - corner_radius: egui::CornerRadius::same(8), + corner_radius: egui::CornerRadius::same(CORNER_RADIUS), shadow: visuals.window_shadow, - inner_margin: egui::Margin::same(16), + inner_margin: egui::Margin::same(INNER_MARGIN), outer_margin: egui::Margin::same(0), }) .show(ctx, |ui| { ui.vertical_centered(|ui| { - ui.heading("General Settings"); - ui.add_space(8.0); + ui.heading("Editor Settings"); + ui.add_space(MEDIUM); ui.horizontal(|ui| { - if ui - .checkbox(&mut app.state_cache, "Cache State") - .on_hover_text("Unsaved changes will be cached between sessions") - .changed() - { - app.save_config(); - if !app.state_cache { - if let Err(e) = TextEditor::clear_state_cache() { - eprintln!("Failed to clear state cache: {e}"); + ui.vertical(|ui| { + if ui + .checkbox(&mut app.state_cache, "Maintain State") + .on_hover_text("Unsaved changes will be cached between sessions") + .changed() + { + app.save_config(); + if !app.state_cache { + if let Err(e) = TextEditor::clear_state_cache() { + eprintln!("Failed to clear state cache: {e}"); + } } } - } + ui.add_space(SMALL); + if ui + .checkbox(&mut app.show_line_numbers, "Show Line Numbers") + .changed() + { + app.save_config(); + } + ui.add_space(SMALL); + if ui + .checkbox(&mut app.auto_hide_toolbar, "Auto Hide Toolbar") + .on_hover_text( + "Hide the top bar until you move your mouse to the upper edge", + ) + .changed() + { + app.save_config(); + } + }); + ui.vertical(|ui| { + if ui.checkbox(&mut app.word_wrap, "Word Wrap").changed() { + app.save_config(); + } + ui.add_space(SMALL); + if ui + .checkbox(&mut app.syntax_highlighting, "Syntax Highlighting") + .changed() + { + app.save_config(); + } + ui.add_space(SMALL); + if ui + .checkbox(&mut app.hide_tab_bar, "Hide Tab Bar") + .on_hover_text( + "Hide the tab bar and show tab title in menu bar instead", + ) + .changed() + { + app.save_config(); + } + }); }); - ui.add_space(4.0); - - ui.horizontal(|ui| { - if ui - .checkbox(&mut app.show_line_numbers, "Show Line Numbers") - .changed() - { - app.save_config(); - } - if ui - .checkbox(&mut app.syntax_highlighting, "Syntax Highlighting") - .changed() - { - app.save_config(); - } - }); - - ui.add_space(4.0); - - ui.horizontal(|ui| { - if ui.checkbox(&mut app.word_wrap, "Word Wrap").changed() { - app.save_config(); - } - }); - - ui.add_space(4.0); - - ui.horizontal(|ui| { - if ui - .checkbox(&mut app.auto_hide_toolbar, "Auto Hide Toolbar") - .on_hover_text("Hide the menu bar until you move your mouse to the top") - .changed() - { - app.save_config(); - } - if ui - .checkbox(&mut app.hide_tab_bar, "Hide Tab Bar") - .on_hover_text("Hide the tab bar and show tab title in menu bar instead") - .changed() - { - app.save_config(); - } - }); - - ui.add_space(12.0); + ui.add_space(SMALL); ui.separator(); + ui.add_space(LARGE); ui.heading("Font Settings"); - ui.add_space(8.0); + ui.add_space(MEDIUM); ui.horizontal(|ui| { - ui.label("Font Family:"); - ui.add_space(5.0); + ui.vertical(|ui| { + ui.label("Font Family:"); + ui.add_space(SMALL); + ui.label("Font Size:"); + }); - let mut changed = false; - egui::ComboBox::from_id_salt("font_family") - .selected_text(&app.font_family) - .show_ui(ui, |ui| { - if ui - .selectable_value( - &mut app.font_family, - "Proportional".to_string(), - "Proportional", - ) - .clicked() - { - changed = true; + ui.vertical(|ui| { + let mut changed = false; + egui::ComboBox::from_id_salt("font_family") + .selected_text(&app.font_family) + .show_ui(ui, |ui| { + if ui + .selectable_value( + &mut app.font_family, + "Proportional".to_string(), + "Proportional", + ) + .clicked() + { + changed = true; + } + if ui + .selectable_value( + &mut app.font_family, + "Monospace".to_string(), + "Monospace", + ) + .clicked() + { + changed = true; + } + }); + + if app.font_size_input.is_none() { + app.font_size_input = Some(app.font_size.to_string()); + } + + let mut font_size_text = app + .font_size_input + .as_ref() + .unwrap_or(&DEFAULT_FONT_SIZE_STR.to_string()) + .to_owned(); + ui.add_space(SMALL); + ui.horizontal(|ui| { + let response = ui.add( + egui::TextEdit::singleline(&mut font_size_text) + .desired_width(FONT_SIZE_INPUT_WIDTH) + .hint_text(DEFAULT_FONT_SIZE_STR) + .id(egui::Id::new("font_size_input")), + ); + + app.font_size_input = Some(font_size_text.to_owned()); + + if response.clicked() { + response.request_focus(); } - if ui - .selectable_value( - &mut app.font_family, - "Monospace".to_string(), - "Monospace", - ) - .clicked() - { - changed = true; + + ui.label("px"); + + if response.lost_focus() { + if let Ok(new_size) = font_size_text.parse::() { + let clamped_size = new_size.clamp(MIN_FONT_SIZE, MAX_FONT_SIZE); + if (app.font_size - clamped_size).abs() > 0.1 { + app.font_size = clamped_size; + app.apply_font_settings(ctx); + } + } + app.font_size_input = None; } - }); - if changed { - app.apply_font_settings(ctx); - } - }); - - ui.add_space(8.0); - - ui.horizontal(|ui| { - ui.label("Font Size:"); - ui.add_space(5.0); - - if app.font_size_input.is_none() { - app.font_size_input = Some(app.font_size.to_string()); - } - - let mut font_size_text = app - .font_size_input - .as_ref() - .unwrap_or(&"14".to_string()) - .to_owned(); - let response = ui.add( - egui::TextEdit::singleline(&mut font_size_text) - .desired_width(50.0) - .hint_text("14") - .id(egui::Id::new("font_size_input")), - ); - - app.font_size_input = Some(font_size_text.to_owned()); - - if response.clicked() { - response.request_focus(); - } - - ui.label("px"); - - if response.lost_focus() { - if let Ok(new_size) = font_size_text.parse::() { - let clamped_size = new_size.clamp(8.0, 32.0); - if (app.font_size - clamped_size).abs() > 0.1 { - app.font_size = clamped_size; + if changed { app.apply_font_settings(ctx); } - } - app.font_size_input = None; - } + }) + }); }); - ui.add_space(8.0); + ui.add_space(MEDIUM); ui.label("Preview:"); - ui.add_space(4.0); + ui.add_space(SMALL); egui::ScrollArea::vertical() - .max_height(150.0) + .max_height(PREVIEW_AREA_MAX_HEIGHT) .show(ui, |ui| { egui::Frame::new() .fill(visuals.code_bg_color) .stroke(visuals.widgets.noninteractive.bg_stroke) - .inner_margin(egui::Margin::same(8)) + .inner_margin(egui::Margin::same(INNER_MARGIN)) .show(ui, |ui| { let preview_font = egui::FontId::new( app.font_size, @@ -211,7 +213,7 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) { }); }); - ui.add_space(12.0); + ui.add_space(LARGE); if ui.button("Close").clicked() { app.show_preferences = false; diff --git a/src/ui/shortcuts_window.rs b/src/ui/shortcuts_window.rs index a3ca874..d4ee8b4 100644 --- a/src/ui/shortcuts_window.rs +++ b/src/ui/shortcuts_window.rs @@ -1,39 +1,42 @@ use crate::app::TextEditor; +use crate::ui::constants::*; use eframe::egui; fn render_shortcuts_content(ui: &mut egui::Ui) { ui.vertical_centered(|ui| { - ui.label(egui::RichText::new("Navigation").size(18.0).strong()); - ui.label(egui::RichText::new("Ctrl + N: New").size(14.0)); - ui.label(egui::RichText::new("Ctrl + O: Open").size(14.0)); - ui.label(egui::RichText::new("Ctrl + S: Save").size(14.0)); - ui.label(egui::RichText::new("Ctrl + Shift + S: Save As").size(14.0)); - ui.label(egui::RichText::new("Ctrl + T: New Tab").size(14.0)); - ui.label(egui::RichText::new("Ctrl + Tab: Next Tab").size(14.0)); - ui.label(egui::RichText::new("Ctrl + Shift + Tab: Last Tab").size(14.0)); - ui.add_space(16.0); + ui.label(egui::RichText::new("Navigation").size(UI_HEADER_SIZE).strong()); + ui.label(egui::RichText::new("Ctrl + N: New").size(UI_TEXT_SIZE)); + ui.label(egui::RichText::new("Ctrl + O: Open").size(UI_TEXT_SIZE)); + ui.label(egui::RichText::new("Ctrl + S: Save").size(UI_TEXT_SIZE)); + ui.label(egui::RichText::new("Ctrl + Shift + S: Save As").size(UI_TEXT_SIZE)); + ui.label(egui::RichText::new("Ctrl + T: New Tab").size(UI_TEXT_SIZE)); + ui.label(egui::RichText::new("Ctrl + W: Close Tab").size(UI_TEXT_SIZE)); + ui.label(egui::RichText::new("Ctrl + Tab: Next Tab").size(UI_TEXT_SIZE)); + ui.label(egui::RichText::new("Ctrl + Shift + Tab: Last Tab").size(UI_TEXT_SIZE)); + ui.add_space(VLARGE); ui.separator(); - ui.label(egui::RichText::new("Editing").size(18.0).strong()); - ui.label(egui::RichText::new("Ctrl + Z: Undo").size(14.0)); - ui.label(egui::RichText::new("Ctrl + Shift + Z: Redo").size(14.0)); - ui.label(egui::RichText::new("Ctrl + X: Cut").size(14.0)); - ui.label(egui::RichText::new("Ctrl + C: Copy").size(14.0)); - ui.label(egui::RichText::new("Ctrl + V: Paste").size(14.0)); - ui.label(egui::RichText::new("Ctrl + A: Select All").size(14.0)); - ui.label(egui::RichText::new("Ctrl + D: Delete Line").size(14.0)); - ui.label(egui::RichText::new("Ctrl + F: Find").size(14.0)); + ui.label(egui::RichText::new("Editing").size(UI_HEADER_SIZE).strong()); + ui.label(egui::RichText::new("Ctrl + Z: Undo").size(UI_TEXT_SIZE)); + ui.label(egui::RichText::new("Ctrl + Shift + Z: Redo").size(UI_TEXT_SIZE)); + ui.label(egui::RichText::new("Ctrl + X: Cut").size(UI_TEXT_SIZE)); + ui.label(egui::RichText::new("Ctrl + C: Copy").size(UI_TEXT_SIZE)); + ui.label(egui::RichText::new("Ctrl + V: Paste").size(UI_TEXT_SIZE)); + ui.label(egui::RichText::new("Ctrl + A: Select All").size(UI_TEXT_SIZE)); + ui.label(egui::RichText::new("Ctrl + D: Delete Line").size(UI_TEXT_SIZE)); + ui.label(egui::RichText::new("Ctrl + F: Find").size(UI_TEXT_SIZE)); + ui.label(egui::RichText::new("Ctrl + R: Replace").size(UI_TEXT_SIZE)); - ui.add_space(16.0); + ui.add_space(VLARGE); ui.separator(); - ui.label(egui::RichText::new("Views").size(18.0).strong()); - ui.label(egui::RichText::new("Ctrl + L: Toggle Line Numbers").size(14.0)); - ui.label(egui::RichText::new("Ctrl + Shift + L: Change Line Number Side").size(14.0)); - ui.label(egui::RichText::new("Ctrl + K: Toggle Word Wrap").size(14.0)); - ui.label(egui::RichText::new("Ctrl + H: Toggle Auto Hide Toolbar").size(14.0)); - ui.label(egui::RichText::new("Ctrl + P: Preferences").size(14.0)); - ui.label(egui::RichText::new("Ctrl + =/-: Increase/Decrease Font Size").size(14.0)); - ui.label(egui::RichText::new("Ctrl + Shift + =/-: Zoom In/Out").size(14.0)); + ui.label(egui::RichText::new("Views").size(UI_HEADER_SIZE).strong()); + ui.label(egui::RichText::new("Ctrl + L: Toggle Line Numbers").size(UI_TEXT_SIZE)); + ui.label(egui::RichText::new("Ctrl + Shift + L: Change Line Number Side").size(UI_TEXT_SIZE)); + ui.label(egui::RichText::new("Ctrl + K: Toggle Word Wrap").size(UI_TEXT_SIZE)); + ui.label(egui::RichText::new("Ctrl + H: Toggle Auto Hide Toolbar").size(UI_TEXT_SIZE)); + ui.label(egui::RichText::new("Ctrl + P: Preferences").size(UI_TEXT_SIZE)); + ui.label(egui::RichText::new("Ctrl + =/-: Increase/Decrease Font Size").size(UI_TEXT_SIZE)); + ui.label(egui::RichText::new("Ctrl + Shift + =/-: Zoom In/Out").size(UI_TEXT_SIZE)); // ui.label( // egui::RichText::new("Ctrl + Shift + .: Toggle Vim Mode") // .size(14.0) @@ -42,7 +45,7 @@ fn render_shortcuts_content(ui: &mut egui::Ui) { // egui::RichText::new("Ctrl + .: Toggle Vim Mode") // .size(14.0) // ); - ui.add_space(16.0); + ui.add_space(VLARGE); ui.separator(); }); } @@ -51,8 +54,8 @@ pub(crate) fn shortcuts_window(app: &mut TextEditor, ctx: &egui::Context) { let visuals = &ctx.style().visuals; let screen_rect = ctx.screen_rect(); - let window_width = (screen_rect.width() * 0.6).clamp(300.0, 400.0); - let window_height = (screen_rect.height() * 0.7).clamp(250.0, 500.0); + let window_width = (screen_rect.width() * WINDOW_WIDTH_RATIO).clamp(WINDOW_MIN_WIDTH, WINDOW_MAX_WIDTH); + let window_height = (screen_rect.height() * WINDOW_HEIGHT_RATIO).clamp(WINDOW_MIN_HEIGHT, WINDOW_MAX_HEIGHT); egui::Window::new("Shortcuts") .collapsible(false) @@ -64,9 +67,9 @@ pub(crate) fn shortcuts_window(app: &mut TextEditor, ctx: &egui::Context) { .frame(egui::Frame { fill: visuals.window_fill, stroke: visuals.window_stroke, - corner_radius: egui::CornerRadius::same(8), + corner_radius: egui::CornerRadius::same(CORNER_RADIUS), shadow: visuals.window_shadow, - inner_margin: egui::Margin::same(16), + inner_margin: egui::Margin::same(INNER_MARGIN), outer_margin: egui::Margin::same(0), }) .show(ctx, |ui| { @@ -85,7 +88,7 @@ pub(crate) fn shortcuts_window(app: &mut TextEditor, ctx: &egui::Context) { ); ui.vertical_centered(|ui| { - ui.add_space(8.0); + ui.add_space(MEDIUM); let visuals = ui.visuals(); let close_button = egui::Button::new("Close") .fill(visuals.widgets.inactive.bg_fill) -- 2.47.1 From eaefb76ce725802d9ad0e63935023bafefbbc258 Mon Sep 17 00:00:00 2001 From: candle Date: Sat, 26 Jul 2025 11:52:10 -0400 Subject: [PATCH 9/9] formatting --- src/ui/constants.rs | 2 +- src/ui/preferences_window.rs | 8 ++++---- src/ui/shortcuts_window.rs | 16 ++++++++++++---- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/ui/constants.rs b/src/ui/constants.rs index c8dd319..e980900 100644 --- a/src/ui/constants.rs +++ b/src/ui/constants.rs @@ -23,4 +23,4 @@ pub const DEFAULT_FONT_SIZE_STR: &str = "14"; pub const PREVIEW_AREA_MAX_HEIGHT: f32 = 150.0; -pub const INNER_MARGIN: i8 = 8; \ No newline at end of file +pub const INNER_MARGIN: i8 = 8; diff --git a/src/ui/preferences_window.rs b/src/ui/preferences_window.rs index b2f94d6..9f40ed6 100644 --- a/src/ui/preferences_window.rs +++ b/src/ui/preferences_window.rs @@ -5,10 +5,10 @@ use eframe::egui; pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) { let visuals = &ctx.style().visuals; let screen_rect = ctx.screen_rect(); - let window_width = (screen_rect.width() * WINDOW_WIDTH_RATIO) - .clamp(WINDOW_MIN_WIDTH, WINDOW_MAX_WIDTH); - let window_height = (screen_rect.height() * WINDOW_HEIGHT_RATIO) - .clamp(WINDOW_MIN_HEIGHT, WINDOW_MAX_HEIGHT); + let window_width = + (screen_rect.width() * WINDOW_WIDTH_RATIO).clamp(WINDOW_MIN_WIDTH, WINDOW_MAX_WIDTH); + let window_height = + (screen_rect.height() * WINDOW_HEIGHT_RATIO).clamp(WINDOW_MIN_HEIGHT, WINDOW_MAX_HEIGHT); let max_size = egui::Vec2::new(window_width, window_height); egui::Window::new("Preferences") diff --git a/src/ui/shortcuts_window.rs b/src/ui/shortcuts_window.rs index d4ee8b4..560baf3 100644 --- a/src/ui/shortcuts_window.rs +++ b/src/ui/shortcuts_window.rs @@ -4,7 +4,11 @@ use eframe::egui; fn render_shortcuts_content(ui: &mut egui::Ui) { ui.vertical_centered(|ui| { - ui.label(egui::RichText::new("Navigation").size(UI_HEADER_SIZE).strong()); + ui.label( + egui::RichText::new("Navigation") + .size(UI_HEADER_SIZE) + .strong(), + ); ui.label(egui::RichText::new("Ctrl + N: New").size(UI_TEXT_SIZE)); ui.label(egui::RichText::new("Ctrl + O: Open").size(UI_TEXT_SIZE)); ui.label(egui::RichText::new("Ctrl + S: Save").size(UI_TEXT_SIZE)); @@ -31,7 +35,9 @@ fn render_shortcuts_content(ui: &mut egui::Ui) { ui.separator(); ui.label(egui::RichText::new("Views").size(UI_HEADER_SIZE).strong()); ui.label(egui::RichText::new("Ctrl + L: Toggle Line Numbers").size(UI_TEXT_SIZE)); - ui.label(egui::RichText::new("Ctrl + Shift + L: Change Line Number Side").size(UI_TEXT_SIZE)); + ui.label( + egui::RichText::new("Ctrl + Shift + L: Change Line Number Side").size(UI_TEXT_SIZE), + ); ui.label(egui::RichText::new("Ctrl + K: Toggle Word Wrap").size(UI_TEXT_SIZE)); ui.label(egui::RichText::new("Ctrl + H: Toggle Auto Hide Toolbar").size(UI_TEXT_SIZE)); ui.label(egui::RichText::new("Ctrl + P: Preferences").size(UI_TEXT_SIZE)); @@ -54,8 +60,10 @@ pub(crate) fn shortcuts_window(app: &mut TextEditor, ctx: &egui::Context) { let visuals = &ctx.style().visuals; let screen_rect = ctx.screen_rect(); - let window_width = (screen_rect.width() * WINDOW_WIDTH_RATIO).clamp(WINDOW_MIN_WIDTH, WINDOW_MAX_WIDTH); - let window_height = (screen_rect.height() * WINDOW_HEIGHT_RATIO).clamp(WINDOW_MIN_HEIGHT, WINDOW_MAX_HEIGHT); + let window_width = + (screen_rect.width() * WINDOW_WIDTH_RATIO).clamp(WINDOW_MIN_WIDTH, WINDOW_MAX_WIDTH); + let window_height = + (screen_rect.height() * WINDOW_HEIGHT_RATIO).clamp(WINDOW_MIN_HEIGHT, WINDOW_MAX_HEIGHT); egui::Window::new("Shortcuts") .collapsible(false) -- 2.47.1