Compare commits

...

2 Commits

15 changed files with 254 additions and 153 deletions

View File

@ -143,7 +143,7 @@ fn get_shortcuts() -> Vec<ShortcutDefinition> {
]
}
fn execute_action(action: ShortcutAction, editor: &mut TextEditor, ctx: &egui::Context) -> bool {
fn execute_action(action: ShortcutAction, editor: &mut TextEditor, _ctx: &egui::Context) -> bool {
match action {
ShortcutAction::NewFile => {
io::new_file(editor);
@ -171,7 +171,9 @@ fn execute_action(action: ShortcutAction, editor: &mut TextEditor, ctx: &egui::C
if let Some(current_tab) = editor.get_active_tab() {
if current_tab.is_modified {
// Show dialog for unsaved changes
editor.pending_unsaved_action = Some(super::state::UnsavedAction::CloseTab(editor.active_tab_index));
editor.pending_unsaved_action = Some(
super::state::UnsavedAction::CloseTab(editor.active_tab_index),
);
} else {
// Close tab directly if no unsaved changes
editor.close_tab(editor.active_tab_index);

View File

@ -1,12 +1,12 @@
use crate::ui::central_panel::central_panel;
use crate::ui::menu_bar::menu_bar;
use crate::ui::tab_bar::tab_bar;
use crate::ui::about_window::about_window;
use crate::ui::shortcuts_window::shortcuts_window;
use crate::ui::preferences_window::preferences_window;
use crate::ui::find_window::find_window;
use crate::app::shortcuts;
use super::editor::TextEditor;
use crate::app::shortcuts;
use crate::ui::about_window::about_window;
use crate::ui::central_panel::central_panel;
use crate::ui::find_window::find_window;
use crate::ui::menu_bar::menu_bar;
use crate::ui::preferences_window::preferences_window;
use crate::ui::shortcuts_window::shortcuts_window;
use crate::ui::tab_bar::tab_bar;
impl eframe::App for TextEditor {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {

View File

@ -30,7 +30,7 @@ impl TextEditor {
tab_bar_rect: None,
menu_bar_stable_until: None,
text_processing_result: std::sync::Arc::new(std::sync::Mutex::new(Default::default())),
processing_thread_handle: None,
_processing_thread_handle: None,
find_query: String::new(),
find_matches: Vec::new(),
current_match_index: None,

View File

@ -19,7 +19,7 @@ impl Default for TextEditor {
show_line_numbers: false,
word_wrap: true,
auto_hide_toolbar: false,
auto_hide_tab_bar: false,
auto_hide_tab_bar: true,
theme: Theme::default(),
line_side: false,
font_family: "Proportional".to_string(),
@ -30,7 +30,7 @@ impl Default for TextEditor {
tab_bar_rect: None,
menu_bar_stable_until: None,
text_processing_result: Arc::new(Mutex::new(TextProcessingResult::default())),
processing_thread_handle: None,
_processing_thread_handle: None,
// Find functionality
find_query: String::new(),
find_matches: Vec::new(),

View File

@ -13,8 +13,8 @@ pub enum UnsavedAction {
#[derive(Clone)]
pub struct TextProcessingResult {
pub line_count: usize,
pub longest_line_index: usize, // Which line is the longest (0-based)
pub longest_line_length: usize, // Character count of the longest line
pub longest_line_index: usize, // Which line is the longest (0-based)
pub longest_line_length: usize, // Character count of the longest line
pub longest_line_pixel_width: f32, // Actual pixel width of the longest line
}
@ -55,7 +55,7 @@ pub struct TextEditor {
pub(crate) tab_bar_rect: Option<egui::Rect>,
pub(crate) menu_bar_stable_until: Option<std::time::Instant>,
pub(crate) text_processing_result: Arc<Mutex<TextProcessingResult>>,
pub(crate) processing_thread_handle: Option<thread::JoinHandle<()>>,
pub(crate) _processing_thread_handle: Option<thread::JoinHandle<()>>,
pub(crate) find_query: String,
pub(crate) find_matches: Vec<(usize, usize)>, // (start_pos, end_pos) byte positions
pub(crate) current_match_index: Option<usize>,
@ -68,7 +68,7 @@ pub struct TextEditor {
// Track previous content for incremental processing
pub(crate) previous_content: String,
pub(crate) previous_cursor_char_index: Option<usize>,
pub(crate) current_cursor_line: usize, // Track current line number incrementally
pub(crate) previous_cursor_line: usize, // Track previous line for comparison
pub(crate) font_settings_changed: bool, // Flag to trigger text reprocessing when font changes
pub(crate) current_cursor_line: usize, // Track current line number incrementally
pub(crate) previous_cursor_line: usize, // Track previous line for comparison
pub(crate) font_settings_changed: bool, // Flag to trigger text reprocessing when font changes
}

View File

@ -17,7 +17,6 @@ impl TextEditor {
return;
}
// Find the longest line by character count first (fast)
let mut longest_line_index = 0;
let mut longest_line_length = 0;
@ -29,17 +28,19 @@ impl TextEditor {
}
}
// Calculate pixel width for the longest line
let font_id = self.get_font_id();
let longest_line_pixel_width = if longest_line_length > 0 {
let longest_line_text = lines[longest_line_index];
ui.fonts(|fonts| {
fonts.layout(
longest_line_text.to_string(),
font_id,
egui::Color32::WHITE,
f32::INFINITY,
).size().x
fonts
.layout(
longest_line_text.to_string(),
font_id,
egui::Color32::WHITE,
f32::INFINITY,
)
.size()
.x
})
} else {
0.0
@ -56,33 +57,60 @@ impl TextEditor {
}
/// Efficiently detect and process line changes without full content iteration
pub fn process_incremental_change(&mut self, old_content: &str, new_content: &str,
old_cursor_pos: usize, new_cursor_pos: usize, ui: &egui::Ui) {
// Calculate cursor line change incrementally
let line_change = self.calculate_cursor_line_change(old_content, new_content, old_cursor_pos, new_cursor_pos);
pub fn process_incremental_change(
&mut self,
old_content: &str,
new_content: &str,
old_cursor_pos: usize,
new_cursor_pos: usize,
ui: &egui::Ui,
) {
let line_change = self.calculate_cursor_line_change(
old_content,
new_content,
old_cursor_pos,
new_cursor_pos,
);
// Update current cursor line
self.current_cursor_line = (self.current_cursor_line as isize + line_change) as usize;
// Detect the type of change and handle appropriately
if old_content.len() == new_content.len() {
// Same length - likely a character replacement
self.handle_character_replacement(old_content, new_content, old_cursor_pos, new_cursor_pos, ui);
self.handle_character_replacement(
old_content,
new_content,
old_cursor_pos,
new_cursor_pos,
ui,
);
} else if new_content.len() > old_content.len() {
// Content added
self.handle_content_addition(old_content, new_content, old_cursor_pos, new_cursor_pos, ui);
self.handle_content_addition(
old_content,
new_content,
old_cursor_pos,
new_cursor_pos,
ui,
);
} else {
// Content removed
self.handle_content_removal(old_content, new_content, old_cursor_pos, new_cursor_pos, ui);
self.handle_content_removal(
old_content,
new_content,
old_cursor_pos,
new_cursor_pos,
ui,
);
}
self.previous_cursor_line = self.current_cursor_line;
}
/// Calculate the change in cursor line without full iteration
fn calculate_cursor_line_change(&self, old_content: &str, new_content: &str,
old_cursor_pos: usize, new_cursor_pos: usize) -> isize {
// Count newlines up to the cursor position in both contents
fn calculate_cursor_line_change(
&self,
old_content: &str,
new_content: &str,
old_cursor_pos: usize,
new_cursor_pos: usize,
) -> isize {
let old_newlines = old_content[..old_cursor_pos.min(old_content.len())]
.bytes()
.filter(|&b| b == b'\n')
@ -97,24 +125,38 @@ impl TextEditor {
}
/// Handle character replacement (same length change)
fn handle_character_replacement(&mut self, old_content: &str, new_content: &str,
old_cursor_pos: usize, new_cursor_pos: usize, ui: &egui::Ui) {
// Extract the current line from new content
fn handle_character_replacement(
&mut self,
_old_content: &str,
new_content: &str,
_old_cursor_pos: usize,
new_cursor_pos: usize,
ui: &egui::Ui,
) {
let current_line = self.extract_current_line(new_content, new_cursor_pos);
let current_line_length = current_line.chars().count();
self.update_line_if_longer(self.current_cursor_line, &current_line, current_line_length, ui);
self.update_line_if_longer(
self.current_cursor_line,
&current_line,
current_line_length,
ui,
);
}
/// Handle content addition
fn handle_content_addition(&mut self, old_content: &str, new_content: &str,
old_cursor_pos: usize, new_cursor_pos: usize, ui: &egui::Ui) {
// Find the common prefix and suffix to identify the added text
fn handle_content_addition(
&mut self,
old_content: &str,
new_content: &str,
_old_cursor_pos: usize,
new_cursor_pos: usize,
ui: &egui::Ui,
) {
let min_len = old_content.len().min(new_content.len());
let mut common_prefix = 0;
let mut common_suffix = 0;
// Find common prefix
for i in 0..min_len {
if old_content.as_bytes()[i] == new_content.as_bytes()[i] {
common_prefix += 1;
@ -123,7 +165,6 @@ impl TextEditor {
}
}
// Find common suffix
for i in 0..min_len - common_prefix {
let old_idx = old_content.len() - 1 - i;
let new_idx = new_content.len() - 1 - i;
@ -134,35 +175,41 @@ impl TextEditor {
}
}
// Extract the added text
let added_start = common_prefix;
let added_end = new_content.len() - common_suffix;
let added_text = &new_content[added_start..added_end];
let newlines_added = added_text.bytes().filter(|&b| b == b'\n').count();
if newlines_added > 0 {
// Lines were added, update line count
let mut current_result = self.get_text_processing_result();
current_result.line_count += newlines_added;
self.update_processing_result(current_result);
}
// Check if the current line is now longer
let current_line = self.extract_current_line(new_content, new_cursor_pos);
let current_line_length = current_line.chars().count();
self.update_line_if_longer(self.current_cursor_line, &current_line, current_line_length, ui);
self.update_line_if_longer(
self.current_cursor_line,
&current_line,
current_line_length,
ui,
);
}
/// Handle content removal
fn handle_content_removal(&mut self, old_content: &str, new_content: &str,
old_cursor_pos: usize, new_cursor_pos: usize, ui: &egui::Ui) {
// Find the common prefix and suffix to identify the removed text
fn handle_content_removal(
&mut self,
old_content: &str,
new_content: &str,
_old_cursor_pos: usize,
new_cursor_pos: usize,
ui: &egui::Ui,
) {
let min_len = old_content.len().min(new_content.len());
let mut common_prefix = 0;
let mut common_suffix = 0;
// Find common prefix
for i in 0..min_len {
if old_content.as_bytes()[i] == new_content.as_bytes()[i] {
common_prefix += 1;
@ -171,7 +218,6 @@ impl TextEditor {
}
}
// Find common suffix
for i in 0..min_len - common_prefix {
let old_idx = old_content.len() - 1 - i;
let new_idx = new_content.len() - 1 - i;
@ -182,41 +228,37 @@ impl TextEditor {
}
}
// Extract the removed text
let removed_start = common_prefix;
let removed_end = old_content.len() - common_suffix;
let removed_text = &old_content[removed_start..removed_end];
let newlines_removed = removed_text.bytes().filter(|&b| b == b'\n').count();
if newlines_removed > 0 {
// Lines were removed, update line count
let mut current_result = self.get_text_processing_result();
current_result.line_count = current_result.line_count.saturating_sub(newlines_removed);
// If we removed the longest line, we need to rescan (but only if necessary)
if self.current_cursor_line <= current_result.longest_line_index {
// The longest line might have been affected, but let's be conservative
// and only rescan if we're sure it was the longest line
if self.current_cursor_line == current_result.longest_line_index {
self.process_text_for_rendering(new_content, ui);
return;
}
self.process_text_for_rendering(new_content, ui);
}
self.update_processing_result(current_result);
}
// Check if the current line changed
let current_line = self.extract_current_line(new_content, new_cursor_pos);
let current_line_length = current_line.chars().count();
// If this was the longest line and it got shorter, we might need to rescan
let current_result = self.get_text_processing_result();
if self.current_cursor_line == current_result.longest_line_index &&
current_line_length < current_result.longest_line_length {
if self.current_cursor_line == current_result.longest_line_index
&& current_line_length < current_result.longest_line_length
{
self.process_text_for_rendering(new_content, ui);
} else {
self.update_line_if_longer(self.current_cursor_line, &current_line, current_line_length, ui);
self.update_line_if_longer(
self.current_cursor_line,
&current_line,
current_line_length,
ui,
);
}
}
@ -224,13 +266,11 @@ impl TextEditor {
fn extract_current_line(&self, content: &str, cursor_pos: usize) -> String {
let bytes = content.as_bytes();
// Find line start (search backwards from cursor)
let mut line_start = cursor_pos;
while line_start > 0 && bytes[line_start - 1] != b'\n' {
line_start -= 1;
}
// Find line end (search forwards from cursor)
let mut line_end = cursor_pos;
while line_end < bytes.len() && bytes[line_end] != b'\n' {
line_end += 1;
@ -240,18 +280,27 @@ impl TextEditor {
}
/// Update longest line info if the current line is longer
fn update_line_if_longer(&mut self, line_index: usize, line_content: &str, line_length: usize, ui: &egui::Ui) {
fn update_line_if_longer(
&mut self,
line_index: usize,
line_content: &str,
line_length: usize,
ui: &egui::Ui,
) {
let current_result = self.get_text_processing_result();
if line_length > current_result.longest_line_length {
let font_id = self.get_font_id();
let pixel_width = ui.fonts(|fonts| {
fonts.layout(
line_content.to_string(),
font_id,
egui::Color32::WHITE,
f32::INFINITY,
).size().x
fonts
.layout(
line_content.to_string(),
font_id,
egui::Color32::WHITE,
f32::INFINITY,
)
.size()
.x
});
let result = TextProcessingResult {

View File

@ -77,7 +77,7 @@ fn get_pywal_colors() -> Option<egui::Visuals> {
let fg = parse_color(colors.get(7).unwrap_or(&colors[0]))?;
let bg_alt = parse_color(colors.get(8).unwrap_or(&colors[0]))?;
let accent = parse_color(colors.get(1).unwrap_or(&colors[0]))?;
let secondary = parse_color(colors.get(2).unwrap_or(&colors[0]))?;
let _secondary = parse_color(colors.get(2).unwrap_or(&colors[0]))?;
let mut visuals = if is_dark_color(bg) {
egui::Visuals::dark()

View File

@ -1,5 +1,5 @@
use crate::app::TextEditor;
use crate::app::tab::Tab;
use crate::app::TextEditor;
use std::fs;
use std::path::PathBuf;

View File

@ -5,7 +5,7 @@ use eframe::egui;
mod app;
mod io;
mod ui;
use app::{TextEditor, config::Config};
use app::{config::Config, TextEditor};
fn main() -> eframe::Result {
let options = eframe::NativeOptions {

View File

@ -14,7 +14,7 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
let line_side = app.line_side;
let font_size = app.font_size;
let output = egui::CentralPanel::default()
let _output = egui::CentralPanel::default()
.frame(egui::Frame::NONE)
.show(ctx, |ui| {
let bg_color = ui.visuals().extreme_bg_color;
@ -23,11 +23,20 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
let editor_height = panel_rect.height();
if !show_line_numbers || app.get_active_tab().is_none() {
egui::ScrollArea::vertical()
.auto_shrink([false; 2])
.show(ui, |ui| {
editor_view_ui(ui, app);
});
let _scroll_response =
egui::ScrollArea::vertical()
.auto_shrink([false; 2])
.show(ui, |ui| {
let full_rect = ui.available_rect_before_wrap();
let context_response =
ui.allocate_response(full_rect.size(), egui::Sense::click());
ui.scope_builder(egui::UiBuilder::new().max_rect(full_rect), |ui| {
editor_view_ui(ui, app);
});
show_context_menu(ui, app, &context_response);
});
return;
}
@ -77,7 +86,8 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
.show(ui, |ui| {
if line_side {
// Line numbers on the right
let text_editor_width = editor_dimensions.text_width + editor_dimensions.total_reserved_width;
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),
@ -87,7 +97,22 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
egui::vec2(editor_dimensions.text_width, editor_height),
egui::Layout::left_to_right(egui::Align::TOP),
|ui| {
editor_view_ui(ui, app);
// Create an invisible interaction area for context menu
let full_rect = ui.available_rect_before_wrap();
let context_response = ui.allocate_response(
full_rect.size(),
egui::Sense::click(),
);
// Reset cursor to render editor at the top
ui.scope_builder(
egui::UiBuilder::new().max_rect(full_rect),
|ui| {
editor_view_ui(ui, app);
},
);
show_context_menu(ui, app, &context_response);
},
);
separator_widget(ui);
@ -96,30 +121,49 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
);
} else {
// Line numbers on the left
let text_editor_width = editor_dimensions.text_width + editor_dimensions.total_reserved_width;
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| {
line_numbers_widget(ui);
separator_widget(ui);
editor_view_ui(ui, app);
// Create an invisible interaction area for context menu
let editor_area = ui.available_rect_before_wrap();
let context_response =
ui.allocate_response(editor_area.size(), egui::Sense::click());
// Reset cursor to render editor at the current position
ui.scope_builder(
egui::UiBuilder::new().max_rect(editor_area),
|ui| {
editor_view_ui(ui, app);
},
);
show_context_menu(ui, app, &context_response);
},
);
}
});
});
}
output.response.context_menu(|ui| {
fn show_context_menu(_ui: &mut egui::Ui, app: &mut TextEditor, context_response: &egui::Response) {
context_response.context_menu(|ui| {
let text_len = app.get_active_tab().unwrap().content.len();
let reset_zoom_key = egui::Id::new("editor_reset_zoom");
if ui.button("Cut").clicked() {
ui.ctx().send_viewport_cmd(egui::ViewportCommand::RequestCut);
ui.ctx()
.send_viewport_cmd(egui::ViewportCommand::RequestCut);
ui.close_menu();
}
if ui.button("Copy").clicked() {
ui.ctx().send_viewport_cmd(egui::ViewportCommand::RequestCopy);
ui.ctx()
.send_viewport_cmd(egui::ViewportCommand::RequestCopy);
ui.close_menu();
}
if ui.button("Paste").clicked() {
@ -159,5 +203,4 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
ui.close_menu();
}
});
}

View File

@ -84,7 +84,6 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
.char_range()
.map(|range| range.primary.index);
if let Some(content) = content_for_processing {
let previous_content = app.previous_content.clone();
let previous_cursor_pos = app.previous_cursor_char_index;

View File

@ -1,6 +1,6 @@
use eframe::egui;
pub(super) fn draw_find_highlight(
pub(super) fn _draw_find_highlight(
ui: &mut egui::Ui,
content: &str,
start_pos: usize,
@ -74,10 +74,7 @@ pub(super) fn draw_find_highlight(
egui::vec2(match_width, line_height),
);
ui.painter().rect_filled(
highlight_rect,
0.0,
ui.visuals().selection.bg_fill,
);
ui.painter()
.rect_filled(highlight_rect, 0.0, ui.visuals().selection.bg_fill);
}
}

View File

@ -86,8 +86,7 @@ pub(super) fn render_line_numbers(
let bg_color = ui.visuals().extreme_bg_color;
let line_numbers_rect = ui.available_rect_before_wrap();
ui.painter()
.rect_filled(line_numbers_rect, 0.0, bg_color);
ui.painter().rect_filled(line_numbers_rect, 0.0, bg_color);
let font_id = egui::FontId::monospace(font_size);
let line_count_width = line_count.to_string().len();

View File

@ -178,8 +178,12 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
app.save_config();
ui.close_menu();
}
if ui.checkbox(&mut app.word_wrap, "Word Wrap").clicked() {
app.save_config();
ui.close_menu();
}
if ui
.checkbox(&mut app.word_wrap, "Toggle Word Wrap")
.checkbox(&mut app.auto_hide_tab_bar, "Hide Tab Bar")
.clicked()
{
app.save_config();
@ -192,13 +196,6 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
app.save_config();
ui.close_menu();
}
if ui
.checkbox(&mut app.auto_hide_tab_bar, "Auto Hide Tab Bar")
.clicked()
{
app.save_config();
ui.close_menu();
}
ui.separator();
@ -282,11 +279,18 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
};
let window_width = ctx.screen_rect().width();
let font_id = ui.style().text_styles[&egui::TextStyle::Body].clone();
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,
ui.style().text_styles[&egui::TextStyle::Body].clone(),
font_id,
ui.style().visuals.text_color(),
))
});

View File

@ -72,7 +72,11 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
app.font_size_input = Some(app.font_size.to_string());
}
let mut font_size_text = app.font_size_input.as_ref().unwrap().clone();
let mut font_size_text = app
.font_size_input
.as_ref()
.unwrap_or(&"14".to_string())
.clone();
let response = ui.add(
egui::TextEdit::singleline(&mut font_size_text)
.desired_width(50.0)
@ -123,8 +127,10 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
},
);
ui.label(
egui::RichText::new("The quick brown fox jumps over the lazy dog.")
.font(preview_font.clone()),
egui::RichText::new(
"The quick brown fox jumps over the lazy dog.",
)
.font(preview_font.clone()),
);
ui.label(
egui::RichText::new("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
@ -134,7 +140,9 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
egui::RichText::new("abcdefghijklmnopqrstuvwxyz")
.font(preview_font.clone()),
);
ui.label(egui::RichText::new("1234567890 !@#$%^&*()").font(preview_font));
ui.label(
egui::RichText::new("1234567890 !@#$%^&*()").font(preview_font),
);
});
});