diff --git a/src/app/shortcuts.rs b/src/app/shortcuts.rs index 660f1ed..294a66b 100644 --- a/src/app/shortcuts.rs +++ b/src/app/shortcuts.rs @@ -15,6 +15,7 @@ enum ShortcutAction { ToggleWordWrap, ToggleAutoHideToolbar, ToggleFind, + ToggleReplace, FocusFind, NextTab, PrevTab, @@ -66,6 +67,11 @@ fn get_shortcuts() -> Vec { egui::Key::F, ShortcutAction::ToggleFind, ), + ( + egui::Modifiers::CTRL, + egui::Key::R, + ShortcutAction::ToggleReplace, + ), ( egui::Modifiers::CTRL | egui::Modifiers::SHIFT, egui::Key::L, @@ -269,6 +275,14 @@ fn execute_action(action: ShortcutAction, editor: &mut TextEditor) -> bool { } false } + ShortcutAction::ToggleReplace => { + editor.show_find = !editor.show_find; + editor.show_replace_section = true; + if editor.show_find && !editor.find_query.is_empty() { + editor.update_find_matches(); + } + false + } ShortcutAction::FocusFind => { if editor.show_find { editor.focus_find = true; diff --git a/src/app/state/ui.rs b/src/app/state/ui.rs index c4f9a6b..ce20a6f 100644 --- a/src/app/state/ui.rs +++ b/src/app/state/ui.rs @@ -5,7 +5,6 @@ use eframe::egui; pub struct EditorDimensions { pub text_width: f32, pub line_number_width: f32, - pub total_reserved_width: f32, } impl TextEditor { @@ -60,7 +59,6 @@ impl TextEditor { return EditorDimensions { text_width: total_available_width, line_number_width: 0.0, - total_reserved_width: 0.0, }; } @@ -78,21 +76,15 @@ impl TextEditor { }); let line_number_width = if self.line_side { - base_line_number_width + 20.0 + base_line_number_width + 25.0 // Scrollbar width } else { - base_line_number_width + 8.0 + base_line_number_width }; - // Separator space (7.0 for separator + 3.0 spacing = 10.0 total) - let separator_width = 10.0; - - let total_reserved_width = line_number_width + separator_width; - let text_width = (total_available_width - total_reserved_width).max(100.0); // Minimum 100px for text - + let text_width = total_available_width.max(100.0); // Minimum 100px for text EditorDimensions { text_width, line_number_width, - total_reserved_width, } } @@ -104,7 +96,7 @@ impl TextEditor { } let longest_line_width = - processing_result.longest_line_pixel_width + (self.font_size * 2.0); + processing_result.longest_line_pixel_width + (self.font_size * 3.0); let dimensions = self.calculate_editor_dimensions(ui); longest_line_width.max(dimensions.text_width) diff --git a/src/ui/central_panel.rs b/src/ui/central_panel.rs index 187874c..b6d3609 100644 --- a/src/ui/central_panel.rs +++ b/src/ui/central_panel.rs @@ -9,13 +9,14 @@ use eframe::egui; use egui::UiKind; use self::editor::editor_view_ui; -use self::line_numbers::{get_visual_line_mapping, render_line_numbers}; +use self::line_numbers::{calculate_visual_line_mapping, render_line_numbers}; pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) { let show_line_numbers = app.show_line_numbers; let word_wrap = app.word_wrap; let line_side = app.line_side; let font_size = app.font_size; + let font_id = app.get_font_id(); let _output = egui::CentralPanel::default() .frame(egui::Frame::NONE) @@ -46,23 +47,23 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) { let line_count = app.get_text_processing_result().line_count; let editor_dimensions = app.calculate_editor_dimensions(ui); let line_number_width = editor_dimensions.line_number_width; + let editor_width = editor_dimensions.text_width - line_number_width; let visual_line_mapping = if word_wrap { - let available_text_width = editor_dimensions.text_width; - if let Some(active_tab) = app.get_active_tab() { - get_visual_line_mapping( - ui, - &active_tab.content, - available_text_width, - font_size, - ) - } else { - vec![] - } + app.get_active_tab() + .map(|active_tab| { + let actual_editor_width = ui.available_width() - line_number_width; + calculate_visual_line_mapping( + ui, + &active_tab.content, + actual_editor_width, + font_id, + ) + }) + .unwrap_or_else(Vec::new) } else { - vec![] + Vec::new() }; - let line_numbers_widget = |ui: &mut egui::Ui| { render_line_numbers( ui, @@ -70,12 +71,12 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) { &visual_line_mapping, line_number_width, word_wrap, + line_side, font_size, ); }; let separator_widget = |ui: &mut egui::Ui| { - 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; @@ -88,17 +89,15 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) { .auto_shrink([false; 2]) .show(ui, |ui| { if line_side { - let text_editor_width = - editor_dimensions.text_width + editor_dimensions.total_reserved_width; ui.allocate_ui_with_layout( - egui::vec2(text_editor_width, editor_height), + egui::vec2(editor_dimensions.text_width, editor_height), egui::Layout::left_to_right(egui::Align::TOP), |ui| { ui.allocate_ui_with_layout( - egui::vec2(editor_dimensions.text_width, editor_height), + egui::vec2(editor_width, editor_height), egui::Layout::left_to_right(egui::Align::TOP), |ui| { - let full_rect = ui.available_rect_before_wrap(); + let full_rect: egui::Rect = ui.available_rect_before_wrap(); let context_response = ui.allocate_response( full_rect.size(), egui::Sense::click(), @@ -119,10 +118,8 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) { }, ); } else { - let text_editor_width = - editor_dimensions.text_width + editor_dimensions.total_reserved_width; ui.allocate_ui_with_layout( - egui::vec2(text_editor_width, editor_height), + egui::vec2(editor_dimensions.text_width, editor_height), egui::Layout::left_to_right(egui::Align::TOP), |ui| { line_numbers_widget(ui); diff --git a/src/ui/central_panel/editor.rs b/src/ui/central_panel/editor.rs index 26784ed..ab54df5 100644 --- a/src/ui/central_panel/editor.rs +++ b/src/ui/central_panel/editor.rs @@ -16,6 +16,10 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R let font_id = app.get_font_id(); let syntax_highlighting_enabled = app.syntax_highlighting; + let bg_color = ui.visuals().extreme_bg_color; + let editor_rect = ui.available_rect_before_wrap(); + ui.painter().rect_filled(editor_rect, 0.0, bg_color); + let reset_zoom_key = egui::Id::new("editor_reset_zoom"); let should_reset_zoom = ui .ctx() @@ -29,10 +33,10 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R }); } - let estimated_width = if !word_wrap { - app.calculate_content_based_width(ui) + let (estimated_width, desired_width) = if !word_wrap { + (app.calculate_content_based_width(ui), f32::INFINITY) } else { - 0.0 + (0.0, ui.available_width()) }; let find_data = if show_find && !app.find_matches.is_empty() { @@ -51,24 +55,7 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R return ui.label("No file open, how did you get here?"); }; - let bg_color = ui.visuals().extreme_bg_color; - let editor_rect = ui.available_rect_before_wrap(); - ui.painter().rect_filled(editor_rect, 0.0, bg_color); - if let Some((content, matches, current_match_index)) = &find_data { - let font_id = ui - .style() - .text_styles - .get(&egui::TextStyle::Monospace) - .unwrap_or(&egui::FontId::monospace(font_size)) - .to_owned(); - - let desired_width = if word_wrap { - ui.available_width() - } else { - f32::INFINITY - }; - let temp_galley = ui.fonts(|fonts| { fonts.layout( content.to_owned(), @@ -78,7 +65,7 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R ) }); - let text_area_left = editor_rect.left() + 4.0; + let text_area_left = editor_rect.left() + 4.0; // Text Editor default margins let text_area_top = editor_rect.top() + 2.0; find_highlight::draw_find_highlights( @@ -93,12 +80,6 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R ); } - let desired_width = if word_wrap { - ui.available_width() - } else { - f32::INFINITY - }; - let language = super::languages::get_language_from_extension(active_tab.file_path.as_deref()); let mut layouter = |ui: &egui::Ui, string: &dyn egui::TextBuffer, wrap_width: f32| { // let syntect_theme = @@ -207,42 +188,40 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R app.text_needs_processing = false; } - if !word_wrap { - if let Some(cursor_pos) = current_cursor_pos { - let cursor_moved = Some(cursor_pos) != app.previous_cursor_position; - let text_changed = output.response.changed(); + if let Some(cursor_pos) = current_cursor_pos { + let cursor_moved = Some(cursor_pos) != app.previous_cursor_position; + let text_changed = output.response.changed(); - if cursor_moved || text_changed { - if let Some(active_tab) = app.get_active_tab() { - let content = &active_tab.content; - let cursor_line = content - .char_indices() - .take_while(|(byte_pos, _)| *byte_pos < cursor_pos) - .filter(|(_, ch)| *ch == '\n') - .count(); + if cursor_moved || text_changed { + if let Some(active_tab) = app.get_active_tab() { + let content = &active_tab.content; + let cursor_line = content + .char_indices() + .take_while(|(byte_pos, _)| *byte_pos < cursor_pos) + .filter(|(_, ch)| *ch == '\n') + .count(); - let font_id = ui - .style() - .text_styles - .get(&egui::TextStyle::Monospace) - .unwrap_or(&egui::FontId::monospace(font_size)) - .to_owned(); - let line_height = ui.fonts(|fonts| fonts.row_height(&font_id)); + let font_id = ui + .style() + .text_styles + .get(&egui::TextStyle::Monospace) + .unwrap_or(&egui::FontId::monospace(font_size)) + .to_owned(); + let line_height = ui.fonts(|fonts| fonts.row_height(&font_id)); - let y_pos = output.response.rect.top() + (cursor_line as f32 * line_height); - let cursor_rect = egui::Rect::from_min_size( - egui::pos2(output.response.rect.left(), y_pos), - egui::vec2(2.0, line_height), - ); + let y_pos = output.response.rect.top() + (cursor_line as f32 * line_height); + let cursor_rect = egui::Rect::from_min_size( + egui::pos2(output.response.rect.left(), y_pos), + egui::vec2(2.0, line_height), + ); - let visible_area = ui.clip_rect(); - if !visible_area.intersects(cursor_rect) { - ui.scroll_to_rect(cursor_rect, Some(egui::Align::Center)); - } + let visible_area = ui.clip_rect(); + if !visible_area.intersects(cursor_rect) { + ui.scroll_to_rect(cursor_rect, Some(egui::Align::Center)); } } - app.previous_cursor_position = Some(cursor_pos); } + app.previous_cursor_position = Some(cursor_pos); } if !output.response.has_focus() diff --git a/src/ui/central_panel/line_numbers.rs b/src/ui/central_panel/line_numbers.rs index daa4ed0..2ecb08f 100644 --- a/src/ui/central_panel/line_numbers.rs +++ b/src/ui/central_panel/line_numbers.rs @@ -1,47 +1,20 @@ use eframe::egui; -thread_local! { - static VISUAL_LINE_MAPPING_CACHE: std::cell::RefCell>)>> = std::cell::RefCell::new(None); -} - -pub(super) fn get_visual_line_mapping( - ui: &egui::Ui, - content: &str, - available_width: f32, - font_size: f32, -) -> Vec> { - let should_recalculate = VISUAL_LINE_MAPPING_CACHE.with(|cache| { - if let Some((cached_content, cached_width, _)) = cache.borrow().as_ref() { - content != cached_content || available_width != *cached_width - } else { - true - } - }); - - if should_recalculate { - let visual_lines = calculate_visual_line_mapping(ui, content, available_width, font_size); - VISUAL_LINE_MAPPING_CACHE.with(|cache| { - *cache.borrow_mut() = Some((content.to_owned(), available_width, visual_lines)); - }); +fn format_line_number(line_number: usize, line_side: bool, line_count_width: usize) -> String { + if line_side { + format!("{:width$}", line_number, width = line_count_width) } - - VISUAL_LINE_MAPPING_CACHE.with(|cache| { - cache - .borrow() - .as_ref() - .map(|(_, _, mapping)| mapping.to_owned()) - .unwrap_or_default() - }) } -fn calculate_visual_line_mapping( +pub(super) fn calculate_visual_line_mapping( ui: &egui::Ui, content: &str, available_width: f32, - font_size: f32, + font_id: egui::FontId, ) -> Vec> { let mut visual_lines = Vec::new(); - let font_id = egui::FontId::monospace(font_size); for (line_num, line) in content.lines().enumerate() { if line.is_empty() { @@ -54,12 +27,11 @@ fn calculate_visual_line_mapping( line.to_string(), font_id.to_owned(), egui::Color32::WHITE, - available_width, + available_width - font_id.size, ) }); let wrapped_line_count = galley.rows.len().max(1); - visual_lines.push(Some(line_num + 1)); for _ in 1..wrapped_line_count { @@ -67,6 +39,11 @@ fn calculate_visual_line_mapping( } } + if content.ends_with('\n') && !content.is_empty() { + let line_num = content.lines().count(); + visual_lines.push(Some(line_num + 1)); + } + visual_lines } @@ -76,12 +53,14 @@ pub(super) fn render_line_numbers( visual_line_mapping: &[Option], line_number_width: f32, word_wrap: bool, + line_side: bool, font_size: f32, ) { ui.vertical(|ui| { + ui.disable(); ui.set_width(line_number_width); ui.spacing_mut().item_spacing.y = 0.0; - + ui.add_space(2.0); // Text Editor default top margin let text_color = ui.visuals().weak_text_color(); let bg_color = ui.visuals().extreme_bg_color; @@ -91,28 +70,28 @@ pub(super) fn render_line_numbers( let font_id = egui::FontId::monospace(font_size); let line_count_width = line_count.to_string().len(); - if word_wrap { - for line_number_opt in visual_line_mapping { - let text = if let Some(line_number) = line_number_opt { - format!("{:>width$}", line_number, width = line_count_width) - } else { - " ".repeat(line_count_width) - }; - ui.label( - egui::RichText::new(text) - .font(font_id.to_owned()) - .color(text_color), - ); - } + let line_texts = if word_wrap { + visual_line_mapping + .into_iter() + .map(|line_number_opt| { + line_number_opt.map_or_else( + || " ".repeat(line_count_width), + |line_number| format_line_number(line_number, line_side, line_count_width), + ) + }) + .collect::>() } else { - for i in 1..=line_count { - let text = format!("{:>width$}", i, width = line_count_width); - ui.label( - egui::RichText::new(text) - .font(font_id.to_owned()) - .color(text_color), - ); - } + (1..=line_count) + .map(|i| format_line_number(i, line_side, line_count_width)) + .collect::>() + }; + + for text in line_texts { + ui.label( + egui::RichText::new(text) + .font(font_id.to_owned()) + .color(text_color), + ); } }); }