Updated dependencies, even better and smarter scrolling, find and replace functionality
This commit is contained in:
parent
b313082374
commit
77eba47f9d
@ -65,7 +65,7 @@ font_size = 16.0
|
||||
In order of importance.
|
||||
| Feature | Info |
|
||||
| ------- | ---- |
|
||||
| **Find/Replace:** | In progress. |
|
||||
| **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. |
|
||||
| **Choose Font** | More than just Monospace/Proportional. |
|
||||
|
||||
@ -15,6 +15,7 @@ enum ShortcutAction {
|
||||
ToggleWordWrap,
|
||||
ToggleAutoHideToolbar,
|
||||
ToggleFind,
|
||||
FocusFind,
|
||||
NextTab,
|
||||
PrevTab,
|
||||
PageUp,
|
||||
@ -55,6 +56,11 @@ fn get_shortcuts() -> Vec<ShortcutDefinition> {
|
||||
egui::Key::W,
|
||||
ShortcutAction::CloseTab,
|
||||
),
|
||||
(
|
||||
egui::Modifiers::CTRL | egui::Modifiers::SHIFT,
|
||||
egui::Key::F,
|
||||
ShortcutAction::FocusFind,
|
||||
),
|
||||
(
|
||||
egui::Modifiers::CTRL,
|
||||
egui::Key::F,
|
||||
@ -167,15 +173,12 @@ fn execute_action(action: ShortcutAction, editor: &mut TextEditor, _ctx: &egui::
|
||||
}
|
||||
ShortcutAction::CloseTab => {
|
||||
if editor.tabs.len() > 1 {
|
||||
// Check if the current tab has unsaved changes
|
||||
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),
|
||||
);
|
||||
} else {
|
||||
// Close tab directly if no unsaved changes
|
||||
editor.close_tab(editor.active_tab_index);
|
||||
}
|
||||
}
|
||||
@ -251,13 +254,25 @@ fn execute_action(action: ShortcutAction, editor: &mut TextEditor, _ctx: &egui::
|
||||
ShortcutAction::Escape => {
|
||||
editor.show_about = false;
|
||||
editor.show_shortcuts = false;
|
||||
if editor.show_find {
|
||||
editor.should_select_current_match = true;
|
||||
}
|
||||
editor.show_find = false;
|
||||
editor.show_preferences = false;
|
||||
editor.pending_unsaved_action = None;
|
||||
false
|
||||
}
|
||||
ShortcutAction::ToggleFind => {
|
||||
//editor.show_find = !editor.show_find;
|
||||
editor.show_find = !editor.show_find;
|
||||
if editor.show_find && !editor.find_query.is_empty() {
|
||||
editor.update_find_matches();
|
||||
}
|
||||
false
|
||||
}
|
||||
ShortcutAction::FocusFind => {
|
||||
if editor.show_find {
|
||||
editor.focus_find = true;
|
||||
}
|
||||
false
|
||||
}
|
||||
ShortcutAction::Preferences => {
|
||||
@ -300,4 +315,9 @@ pub fn handle(editor: &mut TextEditor, ctx: &egui::Context) {
|
||||
if global_zoom_occurred {
|
||||
ctx.set_zoom_factor(editor.zoom_factor);
|
||||
}
|
||||
|
||||
if editor.should_select_current_match {
|
||||
editor.select_current_match(ctx);
|
||||
editor.should_select_current_match = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,7 +46,6 @@ impl eframe::App for TextEditor {
|
||||
self.show_unsaved_changes_dialog(ctx);
|
||||
}
|
||||
|
||||
// Update the previous find state for next frame
|
||||
self.prev_show_find = self.show_find;
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,21 +32,22 @@ impl TextEditor {
|
||||
text_processing_result: std::sync::Arc::new(std::sync::Mutex::new(Default::default())),
|
||||
_processing_thread_handle: None,
|
||||
find_query: String::new(),
|
||||
replace_query: String::new(),
|
||||
find_matches: Vec::new(),
|
||||
current_match_index: None,
|
||||
case_sensitive_search: false,
|
||||
show_replace_section: false,
|
||||
prev_show_find: false,
|
||||
focus_find: false,
|
||||
// vim_mode: config.vim_mode,
|
||||
|
||||
// Cursor tracking for smart scrolling
|
||||
previous_cursor_position: None,
|
||||
|
||||
// Track previous content for incremental processing
|
||||
previous_content: String::new(),
|
||||
previous_cursor_char_index: None,
|
||||
current_cursor_line: 0,
|
||||
previous_cursor_line: 0,
|
||||
font_settings_changed: false,
|
||||
text_needs_processing: false,
|
||||
should_select_current_match: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -31,22 +31,22 @@ impl Default for TextEditor {
|
||||
menu_bar_stable_until: None,
|
||||
text_processing_result: Arc::new(Mutex::new(TextProcessingResult::default())),
|
||||
_processing_thread_handle: None,
|
||||
// Find functionality
|
||||
find_query: String::new(),
|
||||
replace_query: String::new(),
|
||||
find_matches: Vec::new(),
|
||||
current_match_index: None,
|
||||
case_sensitive_search: false,
|
||||
show_replace_section: false,
|
||||
prev_show_find: false,
|
||||
|
||||
// Cursor tracking for smart scrolling
|
||||
focus_find: false,
|
||||
previous_cursor_position: None,
|
||||
|
||||
// Track previous content for incremental processing
|
||||
previous_content: String::new(),
|
||||
previous_cursor_char_index: None,
|
||||
current_cursor_line: 0,
|
||||
previous_cursor_line: 0,
|
||||
font_settings_changed: false,
|
||||
text_needs_processing: false,
|
||||
should_select_current_match: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,9 +13,9 @@ 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_pixel_width: f32, // Actual pixel width of the longest line
|
||||
pub longest_line_index: usize,
|
||||
pub longest_line_length: usize,
|
||||
pub longest_line_pixel_width: f32,
|
||||
}
|
||||
|
||||
impl Default for TextProcessingResult {
|
||||
@ -33,7 +33,7 @@ impl Default for TextProcessingResult {
|
||||
pub struct TextEditor {
|
||||
pub(crate) tabs: Vec<Tab>,
|
||||
pub(crate) active_tab_index: usize,
|
||||
pub(crate) tab_counter: usize, // Counter for numbering new tabs
|
||||
pub(crate) tab_counter: usize,
|
||||
pub(crate) show_about: bool,
|
||||
pub(crate) show_shortcuts: bool,
|
||||
pub(crate) show_find: bool,
|
||||
@ -57,18 +57,19 @@ pub struct TextEditor {
|
||||
pub(crate) text_processing_result: Arc<Mutex<TextProcessingResult>>,
|
||||
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) replace_query: String,
|
||||
pub(crate) find_matches: Vec<(usize, usize)>,
|
||||
pub(crate) current_match_index: Option<usize>,
|
||||
pub(crate) case_sensitive_search: bool,
|
||||
pub(crate) prev_show_find: bool, // Track previous state to detect transitions
|
||||
|
||||
// Cursor tracking for smart scrolling
|
||||
pub(crate) previous_cursor_position: Option<usize>,
|
||||
|
||||
// Track previous content for incremental processing
|
||||
pub(crate) show_replace_section: bool,
|
||||
pub(crate) prev_show_find: bool,
|
||||
pub(crate) focus_find: bool,
|
||||
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,
|
||||
pub(crate) previous_cursor_line: usize,
|
||||
pub(crate) font_settings_changed: bool,
|
||||
pub(crate) text_needs_processing: bool,
|
||||
pub(crate) should_select_current_match: bool,
|
||||
pub(crate) previous_cursor_position: Option<usize>,
|
||||
}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
use super::editor::TextEditor;
|
||||
use eframe::egui;
|
||||
|
||||
impl TextEditor {
|
||||
pub fn update_find_matches(&mut self) {
|
||||
let previous_match_index = self.current_match_index;
|
||||
self.find_matches.clear();
|
||||
self.current_match_index = None;
|
||||
|
||||
@ -32,12 +34,20 @@ impl TextEditor {
|
||||
}
|
||||
|
||||
if !self.find_matches.is_empty() {
|
||||
self.current_match_index = Some(0);
|
||||
if let Some(prev_index) = previous_match_index {
|
||||
if prev_index < self.find_matches.len() {
|
||||
self.current_match_index = Some(prev_index);
|
||||
} else {
|
||||
self.current_match_index = Some(0);
|
||||
}
|
||||
} else {
|
||||
self.current_match_index = Some(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_next(&mut self) {
|
||||
pub fn find_next(&mut self, ctx: &egui::Context) {
|
||||
if self.find_matches.is_empty() {
|
||||
return;
|
||||
}
|
||||
@ -47,9 +57,12 @@ impl TextEditor {
|
||||
} else {
|
||||
self.current_match_index = Some(0);
|
||||
}
|
||||
|
||||
self.select_current_match(ctx);
|
||||
self.should_select_current_match = true;
|
||||
}
|
||||
|
||||
pub fn find_previous(&mut self) {
|
||||
pub fn find_previous(&mut self, ctx: &egui::Context) {
|
||||
if self.find_matches.is_empty() {
|
||||
return;
|
||||
}
|
||||
@ -63,6 +76,9 @@ impl TextEditor {
|
||||
} else {
|
||||
self.current_match_index = Some(0);
|
||||
}
|
||||
|
||||
self.select_current_match(ctx);
|
||||
self.should_select_current_match = true;
|
||||
}
|
||||
|
||||
pub fn get_current_match_position(&self) -> Option<(usize, usize)> {
|
||||
@ -72,4 +88,112 @@ impl TextEditor {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_current_match(&self, ctx: &egui::Context) {
|
||||
if let Some((start_byte, end_byte)) = self.get_current_match_position() {
|
||||
if let Some(active_tab) = self.get_active_tab() {
|
||||
let content = &active_tab.content;
|
||||
|
||||
let start_char = content[..start_byte.min(content.len())].chars().count();
|
||||
let end_char = content[..end_byte.min(content.len())].chars().count();
|
||||
|
||||
let text_edit_id = egui::Id::new("main_text_editor");
|
||||
if let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) {
|
||||
let selection_range = egui::text::CCursorRange::two(
|
||||
egui::text::CCursor::new(start_char),
|
||||
egui::text::CCursor::new(end_char),
|
||||
);
|
||||
state.cursor.set_char_range(Some(selection_range));
|
||||
egui::TextEdit::store_state(ctx, text_edit_id, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn replace_current_match(&mut self, ctx: &egui::Context) {
|
||||
if self.find_query.is_empty() || self.find_matches.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some((start_byte, end_byte)) = self.get_current_match_position() {
|
||||
let replace_query = self.replace_query.clone();
|
||||
let replacement_end = start_byte + replace_query.len();
|
||||
|
||||
if let Some(active_tab) = self.get_active_tab_mut() {
|
||||
let content = &active_tab.content;
|
||||
|
||||
let mut new_content = content.clone();
|
||||
new_content.replace_range(start_byte..end_byte, &replace_query);
|
||||
|
||||
active_tab.content = new_content;
|
||||
active_tab.is_modified = true;
|
||||
}
|
||||
|
||||
self.update_find_matches();
|
||||
|
||||
if let Some(active_tab) = self.get_active_tab() {
|
||||
let replacement_end_char = active_tab.content
|
||||
[..replacement_end.min(active_tab.content.len())]
|
||||
.chars()
|
||||
.count();
|
||||
|
||||
let text_edit_id = egui::Id::new("main_text_editor");
|
||||
if let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) {
|
||||
state
|
||||
.cursor
|
||||
.set_char_range(Some(egui::text::CCursorRange::one(
|
||||
egui::text::CCursor::new(replacement_end_char),
|
||||
)));
|
||||
egui::TextEdit::store_state(ctx, text_edit_id, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn replace_all(&mut self, ctx: &egui::Context) {
|
||||
if self.find_query.is_empty() || self.find_matches.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let find_query = self.find_query.clone();
|
||||
let replace_query = self.replace_query.clone();
|
||||
let case_sensitive = self.case_sensitive_search;
|
||||
let find_matches = self.find_matches.clone();
|
||||
|
||||
if let Some(active_tab) = self.get_active_tab_mut() {
|
||||
let content = &active_tab.content;
|
||||
|
||||
let new_content = if case_sensitive {
|
||||
content.replace(&find_query, &replace_query)
|
||||
} else {
|
||||
let mut result = String::new();
|
||||
let mut last_end = 0;
|
||||
|
||||
for (start_byte, end_byte) in &find_matches {
|
||||
result.push_str(&content[last_end..*start_byte]);
|
||||
result.push_str(&replace_query);
|
||||
last_end = *end_byte;
|
||||
}
|
||||
result.push_str(&content[last_end..]);
|
||||
result
|
||||
};
|
||||
|
||||
active_tab.content = new_content;
|
||||
active_tab.is_modified = true;
|
||||
}
|
||||
|
||||
self.update_find_matches();
|
||||
|
||||
self.current_match_index = None;
|
||||
|
||||
let text_edit_id = egui::Id::new("main_text_editor");
|
||||
if let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) {
|
||||
state
|
||||
.cursor
|
||||
.set_char_range(Some(egui::text::CCursorRange::one(
|
||||
egui::text::CCursor::new(0),
|
||||
)));
|
||||
egui::TextEdit::store_state(ctx, text_edit_id, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,7 +57,7 @@ impl TextEditor {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return; // Should not happen if called correctly
|
||||
return;
|
||||
};
|
||||
|
||||
let visuals = &ctx.style().visuals;
|
||||
|
||||
@ -2,12 +2,12 @@ use super::editor::{TextEditor, TextProcessingResult};
|
||||
use eframe::egui;
|
||||
|
||||
impl TextEditor {
|
||||
/// Process text content and find the longest line (only used for initial scan)
|
||||
pub fn process_text_for_rendering(&mut self, content: &str, ui: &egui::Ui) {
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
let line_count = lines.len().max(1);
|
||||
let line_count = content.bytes().filter(|&b| b == b'\n').count() + 1;
|
||||
|
||||
if lines.is_empty() {
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
|
||||
if content.is_empty() {
|
||||
self.update_processing_result(TextProcessingResult {
|
||||
line_count: 1,
|
||||
longest_line_index: 0,
|
||||
@ -20,6 +20,16 @@ impl TextEditor {
|
||||
let mut longest_line_index = 0;
|
||||
let mut longest_line_length = 0;
|
||||
|
||||
if lines.is_empty() {
|
||||
self.update_processing_result(TextProcessingResult {
|
||||
line_count,
|
||||
longest_line_index: 0,
|
||||
longest_line_length: 0,
|
||||
longest_line_pixel_width: 0.0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
for (index, line) in lines.iter().enumerate() {
|
||||
let char_count = line.chars().count();
|
||||
if char_count > longest_line_length {
|
||||
@ -56,7 +66,6 @@ impl TextEditor {
|
||||
self.update_processing_result(result);
|
||||
}
|
||||
|
||||
/// Efficiently detect and process line changes without full content iteration
|
||||
pub fn process_incremental_change(
|
||||
&mut self,
|
||||
old_content: &str,
|
||||
@ -103,7 +112,6 @@ impl TextEditor {
|
||||
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,
|
||||
@ -124,7 +132,6 @@ impl TextEditor {
|
||||
new_newlines as isize - old_newlines as isize
|
||||
}
|
||||
|
||||
/// Handle character replacement (same length change)
|
||||
fn handle_character_replacement(
|
||||
&mut self,
|
||||
_old_content: &str,
|
||||
@ -144,7 +151,6 @@ impl TextEditor {
|
||||
);
|
||||
}
|
||||
|
||||
/// Handle content addition
|
||||
fn handle_content_addition(
|
||||
&mut self,
|
||||
old_content: &str,
|
||||
@ -183,21 +189,39 @@ impl TextEditor {
|
||||
if newlines_added > 0 {
|
||||
let mut current_result = self.get_text_processing_result();
|
||||
current_result.line_count += newlines_added;
|
||||
self.update_processing_result(current_result);
|
||||
|
||||
let addition_start_line = old_content[..added_start]
|
||||
.bytes()
|
||||
.filter(|&b| b == b'\n')
|
||||
.count();
|
||||
let addition_end_line = old_content[..added_end.min(old_content.len())]
|
||||
.bytes()
|
||||
.filter(|&b| b == b'\n')
|
||||
.count();
|
||||
|
||||
if current_result.longest_line_index >= addition_start_line
|
||||
&& current_result.longest_line_index <= addition_end_line
|
||||
{
|
||||
self.process_text_for_rendering(new_content, ui);
|
||||
} else {
|
||||
if addition_end_line < current_result.longest_line_index {
|
||||
current_result.longest_line_index += newlines_added;
|
||||
}
|
||||
self.update_processing_result(current_result);
|
||||
}
|
||||
} else {
|
||||
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,
|
||||
¤t_line,
|
||||
current_line_length,
|
||||
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,
|
||||
¤t_line,
|
||||
current_line_length,
|
||||
ui,
|
||||
);
|
||||
}
|
||||
|
||||
/// Handle content removal
|
||||
fn handle_content_removal(
|
||||
&mut self,
|
||||
old_content: &str,
|
||||
@ -237,11 +261,27 @@ impl TextEditor {
|
||||
let mut current_result = self.get_text_processing_result();
|
||||
current_result.line_count = current_result.line_count.saturating_sub(newlines_removed);
|
||||
|
||||
if self.current_cursor_line <= current_result.longest_line_index {
|
||||
self.process_text_for_rendering(new_content, ui);
|
||||
}
|
||||
let removal_start_line = old_content[..removed_start]
|
||||
.bytes()
|
||||
.filter(|&b| b == b'\n')
|
||||
.count();
|
||||
let removal_end_line = old_content[..removed_end]
|
||||
.bytes()
|
||||
.filter(|&b| b == b'\n')
|
||||
.count();
|
||||
|
||||
self.update_processing_result(current_result);
|
||||
if current_result.longest_line_index >= removal_start_line
|
||||
&& current_result.longest_line_index <= removal_end_line
|
||||
{
|
||||
self.process_text_for_rendering(new_content, ui);
|
||||
} else {
|
||||
if removal_end_line < current_result.longest_line_index {
|
||||
current_result.longest_line_index = current_result
|
||||
.longest_line_index
|
||||
.saturating_sub(newlines_removed);
|
||||
}
|
||||
self.update_processing_result(current_result);
|
||||
}
|
||||
}
|
||||
|
||||
let current_line = self.extract_current_line(new_content, new_cursor_pos);
|
||||
@ -262,7 +302,6 @@ impl TextEditor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the current line efficiently without full content scan
|
||||
fn extract_current_line(&self, content: &str, cursor_pos: usize) -> String {
|
||||
let bytes = content.as_bytes();
|
||||
|
||||
@ -279,7 +318,6 @@ impl TextEditor {
|
||||
content[line_start..line_end].to_string()
|
||||
}
|
||||
|
||||
/// Update longest line info if the current line is longer
|
||||
fn update_line_if_longer(
|
||||
&mut self,
|
||||
line_index: usize,
|
||||
@ -314,7 +352,6 @@ impl TextEditor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current text processing result
|
||||
pub fn get_text_processing_result(&self) -> TextProcessingResult {
|
||||
self.text_processing_result
|
||||
.lock()
|
||||
@ -322,7 +359,6 @@ impl TextEditor {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Update the processing result atomically
|
||||
fn update_processing_result(&self, result: TextProcessingResult) {
|
||||
if let Ok(mut processing_result) = self.text_processing_result.lock() {
|
||||
*processing_result = result;
|
||||
|
||||
@ -14,6 +14,9 @@ impl TextEditor {
|
||||
self.tab_counter += 1;
|
||||
self.tabs.push(Tab::new_empty(self.tab_counter));
|
||||
self.active_tab_index = self.tabs.len() - 1;
|
||||
if self.show_find && !self.find_query.is_empty() {
|
||||
self.update_find_matches();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_tab(&mut self, tab_index: usize) {
|
||||
@ -24,12 +27,18 @@ impl TextEditor {
|
||||
} else if self.active_tab_index > tab_index {
|
||||
self.active_tab_index -= 1;
|
||||
}
|
||||
if self.show_find && !self.find_query.is_empty() {
|
||||
self.update_find_matches();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn switch_to_tab(&mut self, tab_index: usize) {
|
||||
if tab_index < self.tabs.len() {
|
||||
self.active_tab_index = tab_index;
|
||||
if self.show_find && !self.find_query.is_empty() {
|
||||
self.update_find_matches();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,7 +125,8 @@ impl TextEditor {
|
||||
return self.calculate_editor_dimensions(ui).text_width;
|
||||
}
|
||||
|
||||
let longest_line_width = processing_result.longest_line_pixel_width + (self.font_size * 2.0);
|
||||
let longest_line_width =
|
||||
processing_result.longest_line_pixel_width + (self.font_size * 2.0);
|
||||
|
||||
let dimensions = self.calculate_editor_dimensions(ui);
|
||||
longest_line_width.max(dimensions.text_width)
|
||||
|
||||
@ -60,8 +60,6 @@ impl Tab {
|
||||
}
|
||||
|
||||
pub fn update_modified_state(&mut self) {
|
||||
// Compare current content hash with original content hash to determine if modified
|
||||
// Special case: new_X tabs are only considered modified if they have content
|
||||
if self.title.starts_with("new_") {
|
||||
self.is_modified = !self.content.is_empty();
|
||||
} else {
|
||||
@ -72,7 +70,6 @@ impl Tab {
|
||||
}
|
||||
|
||||
pub fn mark_as_saved(&mut self) {
|
||||
// Update the original content hash to match current content after saving
|
||||
self.original_content_hash = compute_content_hash(&self.content, &mut self.hasher);
|
||||
self.last_content_hash = self.original_content_hash;
|
||||
self.is_modified = false;
|
||||
|
||||
10
src/io.rs
10
src/io.rs
@ -14,7 +14,6 @@ pub(crate) fn open_file(app: &mut TextEditor) {
|
||||
{
|
||||
match fs::read_to_string(&path) {
|
||||
Ok(content) => {
|
||||
// Check if the current active tab is empty/clean and can be replaced
|
||||
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()
|
||||
@ -24,7 +23,6 @@ pub(crate) fn open_file(app: &mut TextEditor) {
|
||||
};
|
||||
|
||||
if should_replace_current_tab {
|
||||
// Replace the current empty tab
|
||||
if let Some(active_tab) = app.get_active_tab_mut() {
|
||||
active_tab.content = content;
|
||||
active_tab.file_path = Some(path.clone());
|
||||
@ -33,13 +31,17 @@ pub(crate) fn open_file(app: &mut TextEditor) {
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("Untitled")
|
||||
.to_string();
|
||||
active_tab.mark_as_saved(); // This will set the hash and mark as not modified
|
||||
active_tab.mark_as_saved();
|
||||
}
|
||||
app.text_needs_processing = true;
|
||||
} else {
|
||||
// Create a new tab as before
|
||||
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();
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
|
||||
@ -11,6 +11,8 @@ pub(crate) fn about_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
.collapsible(false)
|
||||
.resizable(false)
|
||||
.anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
|
||||
.fade_in(true)
|
||||
.fade_out(true)
|
||||
.frame(egui::Frame {
|
||||
fill: visuals.window_fill,
|
||||
stroke: visuals.window_stroke,
|
||||
|
||||
@ -4,6 +4,7 @@ mod line_numbers;
|
||||
|
||||
use crate::app::TextEditor;
|
||||
use eframe::egui;
|
||||
use egui::UiKind;
|
||||
|
||||
use self::editor::editor_view_ui;
|
||||
use self::line_numbers::{get_visual_line_mapping, render_line_numbers};
|
||||
@ -35,7 +36,7 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
editor_view_ui(ui, app);
|
||||
});
|
||||
|
||||
show_context_menu(ui, app, &context_response);
|
||||
handle_empty(ui, app, &context_response);
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -75,7 +76,7 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
ui.add_space(3.0);
|
||||
let separator_x = ui.cursor().left();
|
||||
let mut y_range = ui.available_rect_before_wrap().y_range();
|
||||
y_range.max += 2.0 * font_size; // Extend separator to cover more vertical space
|
||||
y_range.max += 2.0 * font_size;
|
||||
ui.painter()
|
||||
.vline(separator_x, y_range, ui.visuals().window_stroke);
|
||||
ui.add_space(4.0);
|
||||
@ -85,26 +86,22 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
if line_side {
|
||||
// Line numbers on the right
|
||||
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| {
|
||||
// Constrain editor to specific width to leave space for line numbers
|
||||
ui.allocate_ui_with_layout(
|
||||
egui::vec2(editor_dimensions.text_width, editor_height),
|
||||
egui::Layout::left_to_right(egui::Align::TOP),
|
||||
|ui| {
|
||||
// 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| {
|
||||
@ -112,7 +109,7 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
},
|
||||
);
|
||||
|
||||
show_context_menu(ui, app, &context_response);
|
||||
handle_empty(ui, app, &context_response);
|
||||
},
|
||||
);
|
||||
separator_widget(ui);
|
||||
@ -120,7 +117,6 @@ 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;
|
||||
ui.allocate_ui_with_layout(
|
||||
@ -130,12 +126,10 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
line_numbers_widget(ui);
|
||||
separator_widget(ui);
|
||||
|
||||
// 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| {
|
||||
@ -143,7 +137,7 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
},
|
||||
);
|
||||
|
||||
show_context_menu(ui, app, &context_response);
|
||||
handle_empty(ui, app, &context_response);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -151,7 +145,25 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
});
|
||||
}
|
||||
|
||||
fn show_context_menu(_ui: &mut egui::Ui, app: &mut TextEditor, context_response: &egui::Response) {
|
||||
fn handle_empty(_ui: &mut egui::Ui, app: &mut TextEditor, context_response: &egui::Response) {
|
||||
if context_response.clicked() {
|
||||
let text_edit_id = egui::Id::new("main_text_editor");
|
||||
if let Some(mut state) = egui::TextEdit::load_state(_ui.ctx(), text_edit_id) {
|
||||
if let Some(active_tab) = app.get_active_tab() {
|
||||
let text_len = active_tab.content.len();
|
||||
let cursor_pos = egui::text::CCursor::new(text_len);
|
||||
state
|
||||
.cursor
|
||||
.set_char_range(Some(egui::text::CCursorRange::one(cursor_pos)));
|
||||
egui::TextEdit::store_state(_ui.ctx(), text_edit_id, state);
|
||||
|
||||
_ui.ctx().memory_mut(|mem| {
|
||||
mem.request_focus(text_edit_id);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
@ -159,17 +171,17 @@ fn show_context_menu(_ui: &mut egui::Ui, app: &mut TextEditor, context_response:
|
||||
if ui.button("Cut").clicked() {
|
||||
ui.ctx()
|
||||
.send_viewport_cmd(egui::ViewportCommand::RequestCut);
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.button("Copy").clicked() {
|
||||
ui.ctx()
|
||||
.send_viewport_cmd(egui::ViewportCommand::RequestCopy);
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.button("Paste").clicked() {
|
||||
ui.ctx()
|
||||
.send_viewport_cmd(egui::ViewportCommand::RequestPaste);
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.button("Delete").clicked() {
|
||||
ui.ctx().input_mut(|i| {
|
||||
@ -181,7 +193,7 @@ fn show_context_menu(_ui: &mut egui::Ui, app: &mut TextEditor, context_response:
|
||||
modifiers: egui::Modifiers::NONE,
|
||||
})
|
||||
});
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.button("Select All").clicked() {
|
||||
let text_edit_id = egui::Id::new("main_text_editor");
|
||||
@ -193,14 +205,14 @@ fn show_context_menu(_ui: &mut egui::Ui, app: &mut TextEditor, context_response:
|
||||
state.cursor.set_char_range(Some(select_all_range));
|
||||
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
||||
}
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("Reset Zoom").clicked() {
|
||||
ui.ctx().memory_mut(|mem| {
|
||||
mem.data.insert_temp(reset_zoom_key, true);
|
||||
});
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
use crate::app::TextEditor;
|
||||
use eframe::egui;
|
||||
|
||||
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;
|
||||
@ -30,6 +32,18 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
||||
0.0
|
||||
};
|
||||
|
||||
let find_data = if show_find && !app.find_matches.is_empty() {
|
||||
app.get_active_tab().map(|tab| {
|
||||
(
|
||||
tab.content.clone(),
|
||||
app.find_matches.clone(),
|
||||
app.current_match_index,
|
||||
)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let Some(active_tab) = app.get_active_tab_mut() else {
|
||||
return ui.label("No file open, how did you get here?");
|
||||
};
|
||||
@ -38,6 +52,44 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
||||
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))
|
||||
.clone();
|
||||
|
||||
let desired_width = if word_wrap {
|
||||
ui.available_width()
|
||||
} else {
|
||||
f32::INFINITY
|
||||
};
|
||||
|
||||
let temp_galley = ui.fonts(|fonts| {
|
||||
fonts.layout(
|
||||
content.clone(),
|
||||
font_id.clone(),
|
||||
ui.visuals().text_color(),
|
||||
desired_width,
|
||||
)
|
||||
});
|
||||
|
||||
let text_area_left = editor_rect.left() + 4.0;
|
||||
let text_area_top = editor_rect.top() + 2.0;
|
||||
|
||||
find_highlight::draw_find_highlights(
|
||||
ui,
|
||||
content,
|
||||
matches,
|
||||
*current_match_index,
|
||||
&temp_galley,
|
||||
text_area_left,
|
||||
text_area_top,
|
||||
font_size,
|
||||
);
|
||||
}
|
||||
|
||||
let desired_width = if word_wrap {
|
||||
ui.available_width()
|
||||
} else {
|
||||
@ -50,7 +102,7 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
||||
.code_editor()
|
||||
.desired_width(desired_width)
|
||||
.desired_rows(0)
|
||||
.lock_focus(true)
|
||||
.lock_focus(!show_find)
|
||||
.cursor_at_end(false)
|
||||
.id(egui::Id::new("main_text_editor"));
|
||||
|
||||
@ -78,6 +130,10 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
||||
None
|
||||
};
|
||||
|
||||
if content_changed && app.show_find && !app.find_query.is_empty() {
|
||||
app.update_find_matches();
|
||||
}
|
||||
|
||||
let current_cursor_pos = output
|
||||
.state
|
||||
.cursor
|
||||
@ -99,16 +155,9 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
||||
curr_cursor_pos,
|
||||
ui,
|
||||
);
|
||||
} else {
|
||||
app.process_text_for_rendering(&content, ui);
|
||||
|
||||
if let Some(cursor_pos) = current_cursor_pos {
|
||||
app.current_cursor_line = content[..cursor_pos]
|
||||
.bytes()
|
||||
.filter(|&b| b == b'\n')
|
||||
.count();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
app.process_text_for_rendering(&content, ui);
|
||||
}
|
||||
|
||||
app.previous_content = content.clone();
|
||||
@ -120,7 +169,6 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
||||
}
|
||||
}
|
||||
|
||||
// Check if font settings changed and trigger reprocessing
|
||||
if app.font_settings_changed {
|
||||
if let Some(active_tab) = app.get_active_tab() {
|
||||
let content = active_tab.content.clone();
|
||||
@ -131,6 +179,14 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
||||
app.font_settings_changed = false;
|
||||
}
|
||||
|
||||
if app.text_needs_processing {
|
||||
if let Some(active_tab) = app.get_active_tab() {
|
||||
let content = active_tab.content.clone();
|
||||
app.process_text_for_rendering(&content, ui);
|
||||
}
|
||||
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;
|
||||
@ -170,7 +226,6 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
||||
}
|
||||
}
|
||||
|
||||
// Request focus if no dialogs are open
|
||||
if !output.response.has_focus()
|
||||
&& !show_preferences
|
||||
&& !show_about
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
use eframe::egui;
|
||||
|
||||
pub(super) fn _draw_find_highlight(
|
||||
pub(super) fn draw_find_highlights(
|
||||
ui: &mut egui::Ui,
|
||||
content: &str,
|
||||
start_pos: usize,
|
||||
end_pos: usize,
|
||||
editor_rect: egui::Rect,
|
||||
matches: &[(usize, usize)],
|
||||
current_match_index: Option<usize>,
|
||||
galley: &std::sync::Arc<egui::Galley>,
|
||||
text_area_left: f32,
|
||||
text_area_top: f32,
|
||||
font_size: f32,
|
||||
) {
|
||||
let font_id = ui
|
||||
@ -15,10 +17,40 @@ pub(super) fn _draw_find_highlight(
|
||||
.unwrap_or(&egui::FontId::monospace(font_size))
|
||||
.clone();
|
||||
|
||||
let text_up_to_start = &content[..start_pos.min(content.len())];
|
||||
for (match_index, &(start_pos, end_pos)) in matches.iter().enumerate() {
|
||||
let is_current_match = current_match_index == Some(match_index);
|
||||
draw_single_highlight(
|
||||
ui,
|
||||
content,
|
||||
start_pos,
|
||||
end_pos,
|
||||
text_area_left,
|
||||
text_area_top,
|
||||
galley,
|
||||
&font_id,
|
||||
is_current_match,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_single_highlight(
|
||||
ui: &mut egui::Ui,
|
||||
content: &str,
|
||||
start_pos: usize,
|
||||
end_pos: usize,
|
||||
text_area_left: f32,
|
||||
text_area_top: f32,
|
||||
galley: &std::sync::Arc<egui::Galley>,
|
||||
font_id: &egui::FontId,
|
||||
is_current_match: bool,
|
||||
) {
|
||||
let text_up_to_start = &content[..start_pos.min(content.len())];
|
||||
let start_line = text_up_to_start.chars().filter(|&c| c == '\n').count();
|
||||
|
||||
if start_line >= galley.rows.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
let line_start_byte_pos = text_up_to_start.rfind('\n').map(|pos| pos + 1).unwrap_or(0);
|
||||
let line_start_char_pos = content[..line_start_byte_pos].chars().count();
|
||||
let start_char_pos = content[..start_pos].chars().count();
|
||||
@ -32,13 +64,6 @@ pub(super) fn _draw_find_highlight(
|
||||
let line_text = lines[start_line];
|
||||
let text_before_match: String = line_text.chars().take(start_col).collect();
|
||||
|
||||
let line_height = ui.fonts(|fonts| fonts.row_height(&font_id));
|
||||
|
||||
let horizontal_margin = ui.spacing().button_padding.x - 4.0;
|
||||
let vertical_margin = ui.spacing().button_padding.y - 1.0;
|
||||
let text_area_left = editor_rect.left() + horizontal_margin;
|
||||
let text_area_top = editor_rect.top() + vertical_margin;
|
||||
|
||||
let text_before_width = ui.fonts(|fonts| {
|
||||
fonts
|
||||
.layout(
|
||||
@ -51,30 +76,35 @@ pub(super) fn _draw_find_highlight(
|
||||
.x
|
||||
});
|
||||
|
||||
let start_y = text_area_top + (start_line as f32 * line_height);
|
||||
let galley_row = &galley.rows[start_line];
|
||||
let start_y = text_area_top + galley_row.min_y();
|
||||
let line_height = galley_row.height();
|
||||
let start_x = text_area_left + text_before_width;
|
||||
|
||||
{
|
||||
let match_text = &content[start_pos..end_pos.min(content.len())];
|
||||
let match_text = &content[start_pos..end_pos.min(content.len())];
|
||||
let match_width = ui.fonts(|fonts| {
|
||||
fonts
|
||||
.layout(
|
||||
match_text.to_string(),
|
||||
font_id.clone(),
|
||||
ui.visuals().text_color(),
|
||||
f32::INFINITY,
|
||||
)
|
||||
.size()
|
||||
.x
|
||||
});
|
||||
|
||||
let match_width = ui.fonts(|fonts| {
|
||||
fonts
|
||||
.layout(
|
||||
match_text.to_string(),
|
||||
font_id.clone(),
|
||||
ui.visuals().text_color(),
|
||||
f32::INFINITY,
|
||||
)
|
||||
.size()
|
||||
.x
|
||||
});
|
||||
let highlight_rect = egui::Rect::from_min_size(
|
||||
egui::pos2(start_x, start_y),
|
||||
egui::vec2(match_width, line_height),
|
||||
);
|
||||
|
||||
let highlight_rect = egui::Rect::from_min_size(
|
||||
egui::pos2(start_x, start_y),
|
||||
egui::vec2(match_width, line_height),
|
||||
);
|
||||
let highlight_color = if is_current_match {
|
||||
ui.visuals().selection.bg_fill
|
||||
} else {
|
||||
ui.visuals().selection.bg_fill.gamma_multiply(0.6)
|
||||
};
|
||||
|
||||
ui.painter()
|
||||
.rect_filled(highlight_rect, 0.0, ui.visuals().selection.bg_fill);
|
||||
}
|
||||
let painter = ui.painter();
|
||||
painter.rect_filled(highlight_rect, 0.0, highlight_color);
|
||||
}
|
||||
|
||||
@ -6,12 +6,34 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
|
||||
let mut should_close = false;
|
||||
let mut query_changed = false;
|
||||
let mut should_focus_editor = false;
|
||||
|
||||
egui::Window::new("Find")
|
||||
let just_opened = app.show_find && !app.prev_show_find;
|
||||
|
||||
if just_opened && !app.find_query.is_empty() {
|
||||
app.update_find_matches();
|
||||
if app.current_match_index.is_some() {
|
||||
app.select_current_match(ctx);
|
||||
app.should_select_current_match = true;
|
||||
}
|
||||
}
|
||||
|
||||
let focus_requested = ctx.memory(|mem| {
|
||||
mem.data
|
||||
.get_temp::<bool>(egui::Id::new("focus_find_input"))
|
||||
.unwrap_or(false)
|
||||
});
|
||||
|
||||
let top_right_pos = egui::Pos2::new(ctx.available_rect().right(), 22.0);
|
||||
|
||||
egui::Window::new("")
|
||||
.collapsible(false)
|
||||
.resizable(false)
|
||||
.movable(true)
|
||||
.anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
|
||||
.title_bar(false)
|
||||
.default_pos(top_right_pos)
|
||||
.fade_in(true)
|
||||
.fade_out(true)
|
||||
.frame(egui::Frame {
|
||||
fill: visuals.window_fill,
|
||||
stroke: visuals.window_stroke,
|
||||
@ -22,13 +44,20 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
})
|
||||
.show(ctx, |ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.set_min_width(300.0);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let arrow_text = if app.show_replace_section {
|
||||
"⏷"
|
||||
} else {
|
||||
"⏵"
|
||||
};
|
||||
if ui.button(arrow_text).clicked() {
|
||||
app.show_replace_section = !app.show_replace_section;
|
||||
}
|
||||
|
||||
ui.label("Find:");
|
||||
let response = ui.add(
|
||||
egui::TextEdit::singleline(&mut app.find_query)
|
||||
.desired_width(200.0)
|
||||
.desired_width(250.0)
|
||||
.hint_text("Enter search text..."),
|
||||
);
|
||||
|
||||
@ -36,16 +65,29 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
query_changed = true;
|
||||
}
|
||||
|
||||
if !response.has_focus() {
|
||||
if just_opened || focus_requested || app.focus_find {
|
||||
response.request_focus();
|
||||
app.focus_find = false;
|
||||
}
|
||||
|
||||
if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
app.find_next();
|
||||
app.find_next(ctx);
|
||||
response.request_focus();
|
||||
}
|
||||
});
|
||||
|
||||
if app.show_replace_section {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(4.0);
|
||||
ui.label("Replace:");
|
||||
let _replace_response = ui.add(
|
||||
egui::TextEdit::singleline(&mut app.replace_query)
|
||||
.desired_width(250.0)
|
||||
.hint_text("Enter replacement text..."),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
@ -55,6 +97,24 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
if case_sensitive_changed {
|
||||
query_changed = true;
|
||||
}
|
||||
if app.show_replace_section {
|
||||
ui.add_space(8.0);
|
||||
|
||||
let replace_current_enabled =
|
||||
!app.find_matches.is_empty() && app.current_match_index.is_some();
|
||||
ui.add_enabled_ui(replace_current_enabled, |ui| {
|
||||
if ui.button("Replace").clicked() {
|
||||
app.replace_current_match(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
let replace_all_enabled = !app.find_matches.is_empty();
|
||||
ui.add_enabled_ui(replace_all_enabled, |ui| {
|
||||
if ui.button("Replace All").clicked() {
|
||||
app.replace_all(ctx);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
@ -75,7 +135,7 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
ui.label(egui::RichText::new(match_text).weak());
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui.button("✕").clicked() {
|
||||
if ui.button("❌").clicked() {
|
||||
should_close = true;
|
||||
}
|
||||
|
||||
@ -84,14 +144,14 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
let next_enabled = !app.find_matches.is_empty();
|
||||
ui.add_enabled_ui(next_enabled, |ui| {
|
||||
if ui.button("Next").clicked() {
|
||||
app.find_next();
|
||||
app.find_next(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
let prev_enabled = !app.find_matches.is_empty();
|
||||
ui.add_enabled_ui(prev_enabled, |ui| {
|
||||
if ui.button("Previous").clicked() {
|
||||
app.find_previous();
|
||||
app.find_previous(ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -101,21 +161,28 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
|
||||
if query_changed {
|
||||
app.update_find_matches();
|
||||
if app.current_match_index.is_some() {
|
||||
app.select_current_match(ctx);
|
||||
app.should_select_current_match = true;
|
||||
}
|
||||
}
|
||||
|
||||
if should_close {
|
||||
app.select_current_match(ctx);
|
||||
app.should_select_current_match = true;
|
||||
app.show_find = false;
|
||||
}
|
||||
|
||||
ctx.input(|i| {
|
||||
if i.key_pressed(egui::Key::Escape) {
|
||||
app.show_find = false;
|
||||
} else if i.key_pressed(egui::Key::F3) {
|
||||
if i.modifiers.shift {
|
||||
app.find_previous();
|
||||
} else {
|
||||
app.find_next();
|
||||
}
|
||||
if i.key_pressed(egui::Key::Enter) && i.modifiers.ctrl && app.show_find {
|
||||
should_focus_editor = true;
|
||||
app.should_select_current_match = true;
|
||||
}
|
||||
});
|
||||
|
||||
if should_focus_editor {
|
||||
ctx.memory_mut(|mem| {
|
||||
mem.request_focus(egui::Id::new("main_text_editor"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
use crate::{app::TextEditor, io};
|
||||
use eframe::egui::{self, Frame};
|
||||
use egui::UiKind;
|
||||
|
||||
pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
let now = std::time::Instant::now();
|
||||
@ -43,34 +44,34 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
}
|
||||
}
|
||||
|
||||
egui::menu::bar(ui, |ui| {
|
||||
egui::MenuBar::new().ui(ui, |ui| {
|
||||
ui.menu_button("File", |ui| {
|
||||
app.menu_interaction_active = true;
|
||||
if ui.button("New").clicked() {
|
||||
io::new_file(app);
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.button("Open...").clicked() {
|
||||
io::open_file(app);
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("Save").clicked() {
|
||||
io::save_file(app);
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.button("Save As...").clicked() {
|
||||
io::save_as_file(app);
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("Preferences").clicked() {
|
||||
app.show_preferences = true;
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.button("Exit").clicked() {
|
||||
app.request_quit(ctx);
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
});
|
||||
|
||||
@ -78,16 +79,16 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
app.menu_interaction_active = true;
|
||||
if ui.button("Cut").clicked() {
|
||||
ui.ctx().input_mut(|i| i.events.push(egui::Event::Cut));
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.button("Copy").clicked() {
|
||||
ui.ctx().input_mut(|i| i.events.push(egui::Event::Copy));
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.button("Paste").clicked() {
|
||||
ui.ctx()
|
||||
.send_viewport_cmd(egui::ViewportCommand::RequestPaste);
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.button("Delete").clicked() {
|
||||
ui.ctx().input_mut(|i| {
|
||||
@ -99,7 +100,7 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
modifiers: egui::Modifiers::NONE,
|
||||
})
|
||||
});
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.button("Select All").clicked() {
|
||||
let text_edit_id = egui::Id::new("main_text_editor");
|
||||
@ -116,7 +117,7 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
||||
}
|
||||
}
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("Undo").clicked() {
|
||||
@ -138,10 +139,13 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
state.set_undoer(undoer);
|
||||
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
||||
active_tab.update_modified_state();
|
||||
if app.show_find && !app.find_query.is_empty() {
|
||||
app.update_find_matches();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.button("Redo").clicked() {
|
||||
let text_edit_id = egui::Id::new("main_text_editor");
|
||||
@ -162,10 +166,13 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
state.set_undoer(undoer);
|
||||
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
||||
active_tab.update_modified_state();
|
||||
if app.show_find && !app.find_query.is_empty() {
|
||||
app.update_find_matches();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
});
|
||||
|
||||
@ -176,22 +183,22 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
.clicked()
|
||||
{
|
||||
app.save_config();
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.checkbox(&mut app.word_wrap, "Word Wrap").clicked() {
|
||||
app.save_config();
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.checkbox(&mut app.hide_tab_bar, "Hide Tab Bar").clicked() {
|
||||
app.save_config();
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui
|
||||
.checkbox(&mut app.auto_hide_toolbar, "Auto Hide Toolbar")
|
||||
.clicked()
|
||||
{
|
||||
app.save_config();
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
@ -199,7 +206,7 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
if ui.button("Reset Zoom").clicked() {
|
||||
app.zoom_factor = 1.0;
|
||||
ctx.set_zoom_factor(1.0);
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
@ -219,7 +226,7 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
if current_theme != crate::app::theme::Theme::System {
|
||||
app.set_theme(ctx);
|
||||
}
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui
|
||||
.radio_value(
|
||||
@ -232,7 +239,7 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
if current_theme != crate::app::theme::Theme::Light {
|
||||
app.set_theme(ctx);
|
||||
}
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui
|
||||
.radio_value(&mut app.theme, crate::app::theme::Theme::Dark, "Dark")
|
||||
@ -241,16 +248,16 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
if current_theme != crate::app::theme::Theme::Dark {
|
||||
app.set_theme(ctx);
|
||||
}
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
ui.separator();
|
||||
if ui.radio_value(&mut app.line_side, false, "Left").clicked() {
|
||||
app.save_config();
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.radio_value(&mut app.line_side, true, "Right").clicked() {
|
||||
app.save_config();
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -259,11 +266,11 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
app.menu_interaction_active = true;
|
||||
if ui.button("Shortcuts").clicked() {
|
||||
app.show_shortcuts = true;
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.button("About").clicked() {
|
||||
app.show_about = true;
|
||||
ui.close_menu();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -14,6 +14,8 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
.anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
|
||||
.default_open(true)
|
||||
.max_size(max_size)
|
||||
.fade_in(true)
|
||||
.fade_out(true)
|
||||
.frame(egui::Frame {
|
||||
fill: visuals.window_fill,
|
||||
stroke: visuals.window_stroke,
|
||||
|
||||
@ -22,6 +22,7 @@ fn render_shortcuts_content(ui: &mut egui::Ui) {
|
||||
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.add_space(16.0);
|
||||
ui.separator();
|
||||
@ -33,7 +34,6 @@ fn render_shortcuts_content(ui: &mut egui::Ui) {
|
||||
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("Ctrl + Shift + .: Toggle Vim Mode")
|
||||
// .size(14.0)
|
||||
@ -42,7 +42,8 @@ fn render_shortcuts_content(ui: &mut egui::Ui) {
|
||||
// egui::RichText::new("Ctrl + .: Toggle Vim Mode")
|
||||
// .size(14.0)
|
||||
// );
|
||||
ui.add_space(12.0);
|
||||
ui.add_space(16.0);
|
||||
ui.separator();
|
||||
});
|
||||
}
|
||||
|
||||
@ -50,7 +51,6 @@ pub(crate) fn shortcuts_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
let visuals = &ctx.style().visuals;
|
||||
let screen_rect = ctx.screen_rect();
|
||||
|
||||
// Calculate appropriate window size that always fits nicely in the main window
|
||||
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);
|
||||
|
||||
@ -59,6 +59,8 @@ pub(crate) fn shortcuts_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
.resizable(false)
|
||||
.anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
|
||||
.fixed_size([window_width, window_height])
|
||||
.fade_in(true)
|
||||
.fade_out(true)
|
||||
.frame(egui::Frame {
|
||||
fill: visuals.window_fill,
|
||||
stroke: visuals.window_stroke,
|
||||
@ -69,8 +71,7 @@ pub(crate) fn shortcuts_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
})
|
||||
.show(ctx, |ui| {
|
||||
ui.vertical(|ui| {
|
||||
// Scrollable content area
|
||||
let available_height = ui.available_height() - 40.0; // Reserve space for close button
|
||||
let available_height = ui.available_height() - 40.0;
|
||||
ui.allocate_ui_with_layout(
|
||||
[ui.available_width(), available_height].into(),
|
||||
egui::Layout::top_down(egui::Align::Center),
|
||||
@ -83,7 +84,6 @@ pub(crate) fn shortcuts_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
},
|
||||
);
|
||||
|
||||
// Fixed close button at bottom
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add_space(8.0);
|
||||
let visuals = ui.visuals();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user