Tons of minor UI improvements, fix word wrapping, added missing replace shortcut

This commit is contained in:
candle 2025-07-28 23:57:36 -04:00
parent cce83bb0cf
commit a3158129d1
5 changed files with 110 additions and 149 deletions

View File

@ -15,6 +15,7 @@ enum ShortcutAction {
ToggleWordWrap, ToggleWordWrap,
ToggleAutoHideToolbar, ToggleAutoHideToolbar,
ToggleFind, ToggleFind,
ToggleReplace,
FocusFind, FocusFind,
NextTab, NextTab,
PrevTab, PrevTab,
@ -66,6 +67,11 @@ fn get_shortcuts() -> Vec<ShortcutDefinition> {
egui::Key::F, egui::Key::F,
ShortcutAction::ToggleFind, ShortcutAction::ToggleFind,
), ),
(
egui::Modifiers::CTRL,
egui::Key::R,
ShortcutAction::ToggleReplace,
),
( (
egui::Modifiers::CTRL | egui::Modifiers::SHIFT, egui::Modifiers::CTRL | egui::Modifiers::SHIFT,
egui::Key::L, egui::Key::L,
@ -269,6 +275,14 @@ fn execute_action(action: ShortcutAction, editor: &mut TextEditor) -> bool {
} }
false 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 => { ShortcutAction::FocusFind => {
if editor.show_find { if editor.show_find {
editor.focus_find = true; editor.focus_find = true;

View File

@ -5,7 +5,6 @@ use eframe::egui;
pub struct EditorDimensions { pub struct EditorDimensions {
pub text_width: f32, pub text_width: f32,
pub line_number_width: f32, pub line_number_width: f32,
pub total_reserved_width: f32,
} }
impl TextEditor { impl TextEditor {
@ -60,7 +59,6 @@ impl TextEditor {
return EditorDimensions { return EditorDimensions {
text_width: total_available_width, text_width: total_available_width,
line_number_width: 0.0, line_number_width: 0.0,
total_reserved_width: 0.0,
}; };
} }
@ -78,21 +76,15 @@ impl TextEditor {
}); });
let line_number_width = if self.line_side { let line_number_width = if self.line_side {
base_line_number_width + 20.0 base_line_number_width + 25.0 // Scrollbar width
} else { } else {
base_line_number_width + 8.0 base_line_number_width
}; };
// Separator space (7.0 for separator + 3.0 spacing = 10.0 total) let text_width = total_available_width.max(100.0); // Minimum 100px for text
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
EditorDimensions { EditorDimensions {
text_width, text_width,
line_number_width, line_number_width,
total_reserved_width,
} }
} }
@ -104,7 +96,7 @@ impl TextEditor {
} }
let longest_line_width = 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); let dimensions = self.calculate_editor_dimensions(ui);
longest_line_width.max(dimensions.text_width) longest_line_width.max(dimensions.text_width)

View File

@ -9,13 +9,14 @@ use eframe::egui;
use egui::UiKind; use egui::UiKind;
use self::editor::editor_view_ui; 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) { pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
let show_line_numbers = app.show_line_numbers; let show_line_numbers = app.show_line_numbers;
let word_wrap = app.word_wrap; let word_wrap = app.word_wrap;
let line_side = app.line_side; let line_side = app.line_side;
let font_size = app.font_size; let font_size = app.font_size;
let font_id = app.get_font_id();
let _output = egui::CentralPanel::default() let _output = egui::CentralPanel::default()
.frame(egui::Frame::NONE) .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 line_count = app.get_text_processing_result().line_count;
let editor_dimensions = app.calculate_editor_dimensions(ui); let editor_dimensions = app.calculate_editor_dimensions(ui);
let line_number_width = editor_dimensions.line_number_width; 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 visual_line_mapping = if word_wrap {
let available_text_width = editor_dimensions.text_width; app.get_active_tab()
if let Some(active_tab) = app.get_active_tab() { .map(|active_tab| {
get_visual_line_mapping( let actual_editor_width = ui.available_width() - line_number_width;
calculate_visual_line_mapping(
ui, ui,
&active_tab.content, &active_tab.content,
available_text_width, actual_editor_width,
font_size, font_id,
) )
})
.unwrap_or_else(Vec::new)
} else { } else {
vec![] Vec::new()
}
} else {
vec![]
}; };
let line_numbers_widget = |ui: &mut egui::Ui| { let line_numbers_widget = |ui: &mut egui::Ui| {
render_line_numbers( render_line_numbers(
ui, ui,
@ -70,12 +71,12 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
&visual_line_mapping, &visual_line_mapping,
line_number_width, line_number_width,
word_wrap, word_wrap,
line_side,
font_size, font_size,
); );
}; };
let separator_widget = |ui: &mut egui::Ui| { let separator_widget = |ui: &mut egui::Ui| {
ui.add_space(SMALL);
let separator_x = ui.cursor().left(); let separator_x = ui.cursor().left();
let mut y_range = ui.available_rect_before_wrap().y_range(); let mut y_range = ui.available_rect_before_wrap().y_range();
y_range.max += 2.0 * font_size; 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]) .auto_shrink([false; 2])
.show(ui, |ui| { .show(ui, |ui| {
if line_side { 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::Layout::left_to_right(egui::Align::TOP),
|ui| {
ui.allocate_ui_with_layout( ui.allocate_ui_with_layout(
egui::vec2(editor_dimensions.text_width, editor_height), egui::vec2(editor_dimensions.text_width, editor_height),
egui::Layout::left_to_right(egui::Align::TOP), egui::Layout::left_to_right(egui::Align::TOP),
|ui| { |ui| {
let full_rect = ui.available_rect_before_wrap(); ui.allocate_ui_with_layout(
egui::vec2(editor_width, editor_height),
egui::Layout::left_to_right(egui::Align::TOP),
|ui| {
let full_rect: egui::Rect = ui.available_rect_before_wrap();
let context_response = ui.allocate_response( let context_response = ui.allocate_response(
full_rect.size(), full_rect.size(),
egui::Sense::click(), egui::Sense::click(),
@ -119,10 +118,8 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
}, },
); );
} else { } else {
let text_editor_width =
editor_dimensions.text_width + editor_dimensions.total_reserved_width;
ui.allocate_ui_with_layout( 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), egui::Layout::left_to_right(egui::Align::TOP),
|ui| { |ui| {
line_numbers_widget(ui); line_numbers_widget(ui);

View File

@ -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 font_id = app.get_font_id();
let syntax_highlighting_enabled = app.syntax_highlighting; 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 reset_zoom_key = egui::Id::new("editor_reset_zoom");
let should_reset_zoom = ui let should_reset_zoom = ui
.ctx() .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 { let (estimated_width, desired_width) = if !word_wrap {
app.calculate_content_based_width(ui) (app.calculate_content_based_width(ui), f32::INFINITY)
} else { } else {
0.0 (0.0, ui.available_width())
}; };
let find_data = if show_find && !app.find_matches.is_empty() { 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?"); 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 { 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| { let temp_galley = ui.fonts(|fonts| {
fonts.layout( fonts.layout(
content.to_owned(), 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; let text_area_top = editor_rect.top() + 2.0;
find_highlight::draw_find_highlights( 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 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 mut layouter = |ui: &egui::Ui, string: &dyn egui::TextBuffer, wrap_width: f32| {
// let syntect_theme = // let syntect_theme =
@ -207,7 +188,6 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
app.text_needs_processing = false; app.text_needs_processing = false;
} }
if !word_wrap {
if let Some(cursor_pos) = current_cursor_pos { if let Some(cursor_pos) = current_cursor_pos {
let cursor_moved = Some(cursor_pos) != app.previous_cursor_position; let cursor_moved = Some(cursor_pos) != app.previous_cursor_position;
let text_changed = output.response.changed(); let text_changed = output.response.changed();
@ -243,7 +223,6 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
} }
app.previous_cursor_position = Some(cursor_pos); app.previous_cursor_position = Some(cursor_pos);
} }
}
if !output.response.has_focus() if !output.response.has_focus()
&& !show_preferences && !show_preferences

View File

@ -1,47 +1,20 @@
use eframe::egui; use eframe::egui;
thread_local! { fn format_line_number(line_number: usize, line_side: bool, line_count_width: usize) -> String {
static VISUAL_LINE_MAPPING_CACHE: std::cell::RefCell<Option<(String, f32, Vec<Option<usize>>)>> = std::cell::RefCell::new(None); if line_side {
} format!("{:<width$}", line_number, width = line_count_width)
pub(super) fn get_visual_line_mapping(
ui: &egui::Ui,
content: &str,
available_width: f32,
font_size: f32,
) -> Vec<Option<usize>> {
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 { } else {
true format!("{:>width$}", line_number, width = line_count_width)
} }
});
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));
});
} }
VISUAL_LINE_MAPPING_CACHE.with(|cache| { pub(super) fn calculate_visual_line_mapping(
cache
.borrow()
.as_ref()
.map(|(_, _, mapping)| mapping.to_owned())
.unwrap_or_default()
})
}
fn calculate_visual_line_mapping(
ui: &egui::Ui, ui: &egui::Ui,
content: &str, content: &str,
available_width: f32, available_width: f32,
font_size: f32, font_id: egui::FontId,
) -> Vec<Option<usize>> { ) -> Vec<Option<usize>> {
let mut visual_lines = Vec::new(); let mut visual_lines = Vec::new();
let font_id = egui::FontId::monospace(font_size);
for (line_num, line) in content.lines().enumerate() { for (line_num, line) in content.lines().enumerate() {
if line.is_empty() { if line.is_empty() {
@ -54,12 +27,11 @@ fn calculate_visual_line_mapping(
line.to_string(), line.to_string(),
font_id.to_owned(), font_id.to_owned(),
egui::Color32::WHITE, egui::Color32::WHITE,
available_width, available_width - font_id.size,
) )
}); });
let wrapped_line_count = galley.rows.len().max(1); let wrapped_line_count = galley.rows.len().max(1);
visual_lines.push(Some(line_num + 1)); visual_lines.push(Some(line_num + 1));
for _ in 1..wrapped_line_count { 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 visual_lines
} }
@ -76,12 +53,14 @@ pub(super) fn render_line_numbers(
visual_line_mapping: &[Option<usize>], visual_line_mapping: &[Option<usize>],
line_number_width: f32, line_number_width: f32,
word_wrap: bool, word_wrap: bool,
line_side: bool,
font_size: f32, font_size: f32,
) { ) {
ui.vertical(|ui| { ui.vertical(|ui| {
ui.disable();
ui.set_width(line_number_width); ui.set_width(line_number_width);
ui.spacing_mut().item_spacing.y = 0.0; 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 text_color = ui.visuals().weak_text_color();
let bg_color = ui.visuals().extreme_bg_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 font_id = egui::FontId::monospace(font_size);
let line_count_width = line_count.to_string().len(); let line_count_width = line_count.to_string().len();
if word_wrap { let line_texts = if word_wrap {
for line_number_opt in visual_line_mapping { visual_line_mapping
let text = if let Some(line_number) = line_number_opt { .into_iter()
format!("{:>width$}", line_number, width = line_count_width) .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::<Vec<_>>()
} else { } else {
" ".repeat(line_count_width) (1..=line_count)
.map(|i| format_line_number(i, line_side, line_count_width))
.collect::<Vec<_>>()
}; };
for text in line_texts {
ui.label( ui.label(
egui::RichText::new(text) egui::RichText::new(text)
.font(font_id.to_owned()) .font(font_id.to_owned())
.color(text_color), .color(text_color),
); );
} }
} 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),
);
}
}
}); });
} }