hide tab bar, smarter text processing
This commit is contained in:
parent
3918bbff93
commit
1ed06bbe37
@ -6,6 +6,7 @@ use super::theme::Theme;
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub auto_hide_toolbar: bool,
|
pub auto_hide_toolbar: bool,
|
||||||
|
pub auto_hide_tab_bar: bool,
|
||||||
pub show_line_numbers: bool,
|
pub show_line_numbers: bool,
|
||||||
pub word_wrap: bool,
|
pub word_wrap: bool,
|
||||||
pub theme: Theme,
|
pub theme: Theme,
|
||||||
@ -19,6 +20,7 @@ impl Default for Config {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
auto_hide_toolbar: false,
|
auto_hide_toolbar: false,
|
||||||
|
auto_hide_tab_bar: false,
|
||||||
show_line_numbers: false,
|
show_line_numbers: false,
|
||||||
word_wrap: true,
|
word_wrap: true,
|
||||||
theme: Theme::default(),
|
theme: Theme::default(),
|
||||||
@ -52,7 +54,7 @@ impl Config {
|
|||||||
if !config_path.exists() {
|
if !config_path.exists() {
|
||||||
let default_config = Self::default();
|
let default_config = Self::default();
|
||||||
if let Err(e) = default_config.save() {
|
if let Err(e) = default_config.save() {
|
||||||
eprintln!("Failed to create default config file: {}", e);
|
eprintln!("Failed to create default config file: {e}");
|
||||||
}
|
}
|
||||||
return default_config;
|
return default_config;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
|
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 crate::app::shortcuts;
|
||||||
use crate::ui::{
|
|
||||||
about_window::about_window, central_panel::central_panel, find_window::find_window,
|
|
||||||
menu_bar::menu_bar, preferences_window::preferences_window, shortcuts_window::shortcuts_window,
|
|
||||||
tab_bar::tab_bar,
|
|
||||||
};
|
|
||||||
use eframe::egui;
|
|
||||||
|
|
||||||
use super::editor::TextEditor;
|
use super::editor::TextEditor;
|
||||||
|
|
||||||
impl eframe::App for TextEditor {
|
impl eframe::App for TextEditor {
|
||||||
@ -24,77 +24,8 @@ impl eframe::App for TextEditor {
|
|||||||
|
|
||||||
menu_bar(self, ctx);
|
menu_bar(self, ctx);
|
||||||
|
|
||||||
// if self.tabs.len() > 1 {
|
if !self.auto_hide_tab_bar {
|
||||||
tab_bar(self, ctx);
|
tab_bar(self, ctx);
|
||||||
// }
|
|
||||||
|
|
||||||
// Extract data needed for calculations to avoid borrow conflicts
|
|
||||||
let (content_changed, layout_changed, needs_processing) = if let Some(active_tab) = self.get_active_tab() {
|
|
||||||
let content_changed = active_tab.last_content_hash != crate::app::tab::compute_content_hash(&active_tab.content, &mut std::hash::DefaultHasher::new());
|
|
||||||
let layout_changed = self.needs_width_calculation(ctx);
|
|
||||||
(content_changed, layout_changed, true)
|
|
||||||
} else {
|
|
||||||
(false, false, false)
|
|
||||||
};
|
|
||||||
|
|
||||||
if needs_processing {
|
|
||||||
// Only recalculate width when layout parameters change, not on every keystroke
|
|
||||||
if layout_changed {
|
|
||||||
let width = if self.word_wrap {
|
|
||||||
// For word wrap, width only depends on layout parameters
|
|
||||||
let total_width = ctx.available_rect().width();
|
|
||||||
if self.show_line_numbers {
|
|
||||||
let line_count = if let Some(tab) = self.get_active_tab() {
|
|
||||||
tab.content.lines().count().max(1)
|
|
||||||
} else {
|
|
||||||
1
|
|
||||||
};
|
|
||||||
let line_count_digits = line_count.to_string().len();
|
|
||||||
let estimated_char_width = self.font_size * 0.6;
|
|
||||||
let base_line_number_width = line_count_digits as f32 * estimated_char_width;
|
|
||||||
let line_number_width = if self.line_side {
|
|
||||||
base_line_number_width + 20.0
|
|
||||||
} else {
|
|
||||||
base_line_number_width + 8.0
|
|
||||||
};
|
|
||||||
(total_width - line_number_width - 10.0).max(100.0)
|
|
||||||
} else {
|
|
||||||
total_width
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For non-word wrap, use a generous fixed width to avoid constant recalculation
|
|
||||||
// This prevents cursor jumping while still allowing horizontal scrolling
|
|
||||||
let base_width = ctx.available_rect().width();
|
|
||||||
if self.show_line_numbers {
|
|
||||||
let estimated_char_width = self.font_size * 0.6;
|
|
||||||
let line_number_width = if self.line_side { 60.0 } else { 40.0 };
|
|
||||||
(base_width - line_number_width - 10.0).max(100.0)
|
|
||||||
} else {
|
|
||||||
base_width
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
self.update_width_calculation_state(ctx, width);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process text changes using stable cached width
|
|
||||||
if content_changed {
|
|
||||||
if let Some(active_tab) = self.get_active_tab() {
|
|
||||||
let content = active_tab.content.clone();
|
|
||||||
let word_wrap = self.word_wrap;
|
|
||||||
let cached_width = self.get_cached_width();
|
|
||||||
let available_width = cached_width.unwrap_or_else(|| {
|
|
||||||
// Initialize with a reasonable default if no cache exists
|
|
||||||
if word_wrap {
|
|
||||||
ctx.available_rect().width()
|
|
||||||
} else {
|
|
||||||
ctx.available_rect().width()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.process_text_for_rendering(&content, available_width);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
central_panel(self, ctx);
|
central_panel(self, ctx);
|
||||||
@ -117,6 +48,5 @@ impl eframe::App for TextEditor {
|
|||||||
|
|
||||||
// Update the previous find state for next frame
|
// Update the previous find state for next frame
|
||||||
self.prev_show_find = self.show_find;
|
self.prev_show_find = self.show_find;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ impl TextEditor {
|
|||||||
show_line_numbers: config.show_line_numbers,
|
show_line_numbers: config.show_line_numbers,
|
||||||
word_wrap: config.word_wrap,
|
word_wrap: config.word_wrap,
|
||||||
auto_hide_toolbar: config.auto_hide_toolbar,
|
auto_hide_toolbar: config.auto_hide_toolbar,
|
||||||
|
auto_hide_tab_bar: config.auto_hide_tab_bar,
|
||||||
theme: config.theme,
|
theme: config.theme,
|
||||||
line_side: config.line_side,
|
line_side: config.line_side,
|
||||||
font_family: config.font_family,
|
font_family: config.font_family,
|
||||||
@ -35,17 +36,16 @@ impl TextEditor {
|
|||||||
current_match_index: None,
|
current_match_index: None,
|
||||||
case_sensitive_search: false,
|
case_sensitive_search: false,
|
||||||
prev_show_find: false,
|
prev_show_find: false,
|
||||||
// Width calculation cache and state tracking
|
|
||||||
cached_width: None,
|
|
||||||
last_word_wrap: config.word_wrap,
|
|
||||||
last_show_line_numbers: config.show_line_numbers,
|
|
||||||
last_font_size: config.font_size,
|
|
||||||
last_line_side: config.line_side,
|
|
||||||
last_viewport_width: 0.0,
|
|
||||||
// vim_mode: config.vim_mode,
|
// vim_mode: config.vim_mode,
|
||||||
|
|
||||||
// Cursor tracking for smart scrolling
|
// Cursor tracking for smart scrolling
|
||||||
previous_cursor_position: None,
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,8 +72,6 @@ impl TextEditor {
|
|||||||
|
|
||||||
editor.apply_font_settings(&cc.egui_ctx);
|
editor.apply_font_settings(&cc.egui_ctx);
|
||||||
|
|
||||||
editor.start_text_processing_thread();
|
|
||||||
|
|
||||||
editor
|
editor
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,6 +79,7 @@ impl TextEditor {
|
|||||||
Config {
|
Config {
|
||||||
auto_hide_toolbar: self.auto_hide_toolbar,
|
auto_hide_toolbar: self.auto_hide_toolbar,
|
||||||
show_line_numbers: self.show_line_numbers,
|
show_line_numbers: self.show_line_numbers,
|
||||||
|
auto_hide_tab_bar: self.auto_hide_tab_bar,
|
||||||
word_wrap: self.word_wrap,
|
word_wrap: self.word_wrap,
|
||||||
theme: self.theme,
|
theme: self.theme,
|
||||||
line_side: self.line_side,
|
line_side: self.line_side,
|
||||||
@ -93,7 +92,7 @@ impl TextEditor {
|
|||||||
pub fn save_config(&self) {
|
pub fn save_config(&self) {
|
||||||
let config = self.get_config();
|
let config = self.get_config();
|
||||||
if let Err(e) = config.save() {
|
if let Err(e) = config.save() {
|
||||||
eprintln!("Failed to save configuration: {}", e);
|
eprintln!("Failed to save configuration: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ impl Default for TextEditor {
|
|||||||
show_line_numbers: false,
|
show_line_numbers: false,
|
||||||
word_wrap: true,
|
word_wrap: true,
|
||||||
auto_hide_toolbar: false,
|
auto_hide_toolbar: false,
|
||||||
|
auto_hide_tab_bar: false,
|
||||||
theme: Theme::default(),
|
theme: Theme::default(),
|
||||||
line_side: false,
|
line_side: false,
|
||||||
font_family: "Proportional".to_string(),
|
font_family: "Proportional".to_string(),
|
||||||
@ -36,17 +37,15 @@ impl Default for TextEditor {
|
|||||||
current_match_index: None,
|
current_match_index: None,
|
||||||
case_sensitive_search: false,
|
case_sensitive_search: false,
|
||||||
prev_show_find: false,
|
prev_show_find: false,
|
||||||
// Width calculation cache and state tracking
|
|
||||||
cached_width: None,
|
|
||||||
last_word_wrap: true,
|
|
||||||
last_show_line_numbers: false,
|
|
||||||
last_font_size: 14.0,
|
|
||||||
last_line_side: false,
|
|
||||||
last_viewport_width: 0.0,
|
|
||||||
// vim_mode: false,
|
|
||||||
|
|
||||||
// Cursor tracking for smart scrolling
|
// Cursor tracking for smart scrolling
|
||||||
previous_cursor_position: None,
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,18 +13,18 @@ pub enum UnsavedAction {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TextProcessingResult {
|
pub struct TextProcessingResult {
|
||||||
pub line_count: usize,
|
pub line_count: usize,
|
||||||
pub visual_line_mapping: Vec<Option<usize>>,
|
pub longest_line_index: usize, // Which line is the longest (0-based)
|
||||||
pub max_line_length: f32,
|
pub longest_line_length: usize, // Character count of the longest line
|
||||||
pub _processed_content: String,
|
pub longest_line_pixel_width: f32, // Actual pixel width of the longest line
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TextProcessingResult {
|
impl Default for TextProcessingResult {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
line_count: 1,
|
line_count: 1,
|
||||||
visual_line_mapping: vec![Some(1)],
|
longest_line_index: 0,
|
||||||
max_line_length: 0.0,
|
longest_line_length: 0,
|
||||||
_processed_content: String::new(),
|
longest_line_pixel_width: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -44,6 +44,7 @@ pub struct TextEditor {
|
|||||||
pub(crate) show_line_numbers: bool,
|
pub(crate) show_line_numbers: bool,
|
||||||
pub(crate) word_wrap: bool,
|
pub(crate) word_wrap: bool,
|
||||||
pub(crate) auto_hide_toolbar: bool,
|
pub(crate) auto_hide_toolbar: bool,
|
||||||
|
pub(crate) auto_hide_tab_bar: bool,
|
||||||
pub(crate) theme: Theme,
|
pub(crate) theme: Theme,
|
||||||
pub(crate) line_side: bool,
|
pub(crate) line_side: bool,
|
||||||
pub(crate) font_family: String,
|
pub(crate) font_family: String,
|
||||||
@ -61,15 +62,12 @@ pub struct TextEditor {
|
|||||||
pub(crate) case_sensitive_search: bool,
|
pub(crate) case_sensitive_search: bool,
|
||||||
pub(crate) prev_show_find: bool, // Track previous state to detect transitions
|
pub(crate) prev_show_find: bool, // Track previous state to detect transitions
|
||||||
|
|
||||||
// Width calculation cache and state tracking
|
|
||||||
pub(crate) cached_width: Option<f32>,
|
|
||||||
pub(crate) last_word_wrap: bool,
|
|
||||||
pub(crate) last_show_line_numbers: bool,
|
|
||||||
pub(crate) last_font_size: f32,
|
|
||||||
pub(crate) last_line_side: bool,
|
|
||||||
pub(crate) last_viewport_width: f32,
|
|
||||||
// pub(crate) vim_mode: bool,
|
|
||||||
|
|
||||||
// Cursor tracking for smart scrolling
|
// Cursor tracking for smart scrolling
|
||||||
pub(crate) previous_cursor_position: Option<usize>,
|
pub(crate) previous_cursor_position: Option<usize>,
|
||||||
|
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -79,7 +79,7 @@ impl TextEditor {
|
|||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
|
|
||||||
for file in &files_to_list {
|
for file in &files_to_list {
|
||||||
ui.label(egui::RichText::new(format!("• {}", file)).size(18.0).weak());
|
ui.label(egui::RichText::new(format!("• {file}")).size(18.0).weak());
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.add_space(12.0);
|
ui.add_space(12.0);
|
||||||
|
|||||||
@ -1,63 +1,282 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
use std::thread;
|
|
||||||
|
|
||||||
use super::editor::{TextEditor, TextProcessingResult};
|
use super::editor::{TextEditor, TextProcessingResult};
|
||||||
|
use eframe::egui;
|
||||||
|
|
||||||
impl TextEditor {
|
impl TextEditor {
|
||||||
pub fn start_text_processing_thread(&mut self) {
|
/// Process text content and find the longest line (only used for initial scan)
|
||||||
let _processing_result = Arc::clone(&self.text_processing_result);
|
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 handle = thread::Builder::new()
|
if lines.is_empty() {
|
||||||
.name("TextProcessor".to_string())
|
self.update_processing_result(TextProcessingResult {
|
||||||
.spawn(move || {
|
line_count: 1,
|
||||||
// Set thread priority to high (platform-specific)
|
longest_line_index: 0,
|
||||||
#[cfg(target_os = "linux")]
|
longest_line_length: 0,
|
||||||
{
|
longest_line_pixel_width: 0.0,
|
||||||
unsafe {
|
|
||||||
let thread_id = libc::pthread_self();
|
|
||||||
let mut param: libc::sched_param = std::mem::zeroed();
|
|
||||||
param.sched_priority = 50;
|
|
||||||
let _ = libc::pthread_setschedparam(thread_id, libc::SCHED_OTHER, ¶m);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
match handle {
|
// Find the longest line by character count first (fast)
|
||||||
Ok(h) => self.processing_thread_handle = Some(h),
|
let mut longest_line_index = 0;
|
||||||
Err(e) => eprintln!("Failed to start text processing thread: {}", e),
|
let mut longest_line_length = 0;
|
||||||
|
|
||||||
|
for (index, line) in lines.iter().enumerate() {
|
||||||
|
let char_count = line.chars().count();
|
||||||
|
if char_count > longest_line_length {
|
||||||
|
longest_line_length = char_count;
|
||||||
|
longest_line_index = index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn process_text_for_rendering(
|
// Calculate pixel width for the longest line
|
||||||
&mut self,
|
let font_id = self.get_font_id();
|
||||||
content: &str,
|
let longest_line_pixel_width = if longest_line_length > 0 {
|
||||||
available_width: f32,
|
let longest_line_text = lines[longest_line_index];
|
||||||
) {
|
ui.fonts(|fonts| {
|
||||||
let line_count = content.lines().count().max(1);
|
fonts.layout(
|
||||||
|
longest_line_text.to_string(),
|
||||||
let visual_line_mapping = if self.word_wrap {
|
font_id,
|
||||||
// For now, simplified mapping - this could be moved to background thread
|
egui::Color32::WHITE,
|
||||||
(1..=line_count).map(Some).collect()
|
f32::INFINITY,
|
||||||
|
).size().x
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
(1..=line_count).map(Some).collect()
|
0.0
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = TextProcessingResult {
|
let result = TextProcessingResult {
|
||||||
line_count,
|
line_count,
|
||||||
visual_line_mapping,
|
longest_line_index,
|
||||||
max_line_length: available_width,
|
longest_line_length,
|
||||||
_processed_content: content.to_string(),
|
longest_line_pixel_width,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Ok(mut processing_result) = self.text_processing_result.lock() {
|
self.update_processing_result(result);
|
||||||
*processing_result = result;
|
}
|
||||||
|
|
||||||
|
/// 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);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
} 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);
|
||||||
|
} else {
|
||||||
|
// Content removed
|
||||||
|
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
|
||||||
|
let old_newlines = old_content[..old_cursor_pos.min(old_content.len())]
|
||||||
|
.bytes()
|
||||||
|
.filter(|&b| b == b'\n')
|
||||||
|
.count();
|
||||||
|
|
||||||
|
let new_newlines = new_content[..new_cursor_pos.min(new_content.len())]
|
||||||
|
.bytes()
|
||||||
|
.filter(|&b| b == b'\n')
|
||||||
|
.count();
|
||||||
|
|
||||||
|
new_newlines as isize - old_newlines as isize
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
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 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
|
||||||
|
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;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
if old_content.as_bytes()[old_idx] == new_content.as_bytes()[new_idx] {
|
||||||
|
common_suffix += 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, ¤t_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
|
||||||
|
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;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
if old_content.as_bytes()[old_idx] == new_content.as_bytes()[new_idx] {
|
||||||
|
common_suffix += 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.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 {
|
||||||
|
self.process_text_for_rendering(new_content, ui);
|
||||||
|
} else {
|
||||||
|
self.update_line_if_longer(self.current_cursor_line, ¤t_line, current_line_length, ui);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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();
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 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
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = TextProcessingResult {
|
||||||
|
line_count: current_result.line_count,
|
||||||
|
longest_line_index: line_index,
|
||||||
|
longest_line_length: line_length,
|
||||||
|
longest_line_pixel_width: pixel_width,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.update_processing_result(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current text processing result
|
||||||
pub fn get_text_processing_result(&self) -> TextProcessingResult {
|
pub fn get_text_processing_result(&self) -> TextProcessingResult {
|
||||||
self.text_processing_result
|
self.text_processing_result
|
||||||
.lock()
|
.lock()
|
||||||
.map(|result| result.clone())
|
.map(|result| result.clone())
|
||||||
.unwrap_or_default()
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,14 +12,19 @@ impl TextEditor {
|
|||||||
pub fn get_title(&self) -> String {
|
pub fn get_title(&self) -> String {
|
||||||
if let Some(tab) = self.get_active_tab() {
|
if let Some(tab) = self.get_active_tab() {
|
||||||
let modified_indicator = if tab.is_modified { "*" } else { "" };
|
let modified_indicator = if tab.is_modified { "*" } else { "" };
|
||||||
format!("{}{} - C-Text", tab.title, modified_indicator)
|
format!(
|
||||||
|
"{}{} - {}",
|
||||||
|
tab.title,
|
||||||
|
modified_indicator,
|
||||||
|
env!("CARGO_PKG_NAME")
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
"C-Text".to_string()
|
format!("{} - {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the configured font ID based on the editor's font settings
|
/// Get the configured font ID based on the editor's font settings
|
||||||
fn get_font_id(&self) -> egui::FontId {
|
pub fn get_font_id(&self) -> egui::FontId {
|
||||||
let font_family = match self.font_family.as_str() {
|
let font_family = match self.font_family.as_str() {
|
||||||
"Monospace" => egui::FontFamily::Monospace,
|
"Monospace" => egui::FontFamily::Monospace,
|
||||||
_ => egui::FontFamily::Proportional,
|
_ => egui::FontFamily::Proportional,
|
||||||
@ -77,9 +82,9 @@ impl TextEditor {
|
|||||||
|
|
||||||
// Add padding based on line_side setting
|
// Add padding based on line_side setting
|
||||||
let line_number_width = if self.line_side {
|
let line_number_width = if self.line_side {
|
||||||
base_line_number_width + 20.0 // Extra padding when line numbers are on the side
|
base_line_number_width + 20.0
|
||||||
} else {
|
} else {
|
||||||
base_line_number_width + 8.0 // Minimal padding when line numbers are normal
|
base_line_number_width + 8.0
|
||||||
};
|
};
|
||||||
|
|
||||||
// Separator space (7.0 for separator + 3.0 spacing = 10.0 total)
|
// Separator space (7.0 for separator + 3.0 spacing = 10.0 total)
|
||||||
@ -95,68 +100,19 @@ impl TextEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate the available width for non-word-wrapped content based on content analysis
|
/// Calculate the available width for non-word-wrapped content based on processed text data
|
||||||
pub fn calculate_content_based_width(&self, ui: &egui::Ui) -> f32 {
|
pub fn calculate_content_based_width(&self, ui: &egui::Ui) -> f32 {
|
||||||
if let Some(active_tab) = self.get_active_tab() {
|
let processing_result = self.get_text_processing_result();
|
||||||
let content = &active_tab.content;
|
|
||||||
|
|
||||||
if content.is_empty() {
|
if processing_result.longest_line_length == 0 {
|
||||||
return self.calculate_editor_dimensions(ui).text_width;
|
return self.calculate_editor_dimensions(ui).text_width;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the longest line
|
// Use the pre-calculated pixel width with some padding
|
||||||
let longest_line = content
|
let longest_line_width = processing_result.longest_line_pixel_width + 20.0;
|
||||||
.lines()
|
|
||||||
.max_by_key(|line| line.chars().count())
|
|
||||||
.unwrap_or("");
|
|
||||||
|
|
||||||
if longest_line.is_empty() {
|
|
||||||
return self.calculate_editor_dimensions(ui).text_width;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the width needed for the longest line
|
|
||||||
let font_id = self.get_font_id();
|
|
||||||
let longest_line_width = ui.fonts(|fonts| {
|
|
||||||
fonts.layout(
|
|
||||||
longest_line.to_string(),
|
|
||||||
font_id,
|
|
||||||
egui::Color32::WHITE,
|
|
||||||
f32::INFINITY,
|
|
||||||
).size().x
|
|
||||||
}) + 20.0; // Add some padding
|
|
||||||
|
|
||||||
// Return the larger of the calculated width or minimum available width
|
// Return the larger of the calculated width or minimum available width
|
||||||
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)
|
||||||
} else {
|
|
||||||
self.calculate_editor_dimensions(ui).text_width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if width calculation needs to be performed based on parameter changes
|
|
||||||
pub fn needs_width_calculation(&self, ctx: &egui::Context) -> bool {
|
|
||||||
let current_viewport_width = ctx.available_rect().width();
|
|
||||||
|
|
||||||
self.cached_width.is_none() ||
|
|
||||||
self.word_wrap != self.last_word_wrap ||
|
|
||||||
self.show_line_numbers != self.last_show_line_numbers ||
|
|
||||||
(self.font_size - self.last_font_size).abs() > 0.1 ||
|
|
||||||
self.line_side != self.last_line_side ||
|
|
||||||
(current_viewport_width - self.last_viewport_width).abs() > 1.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the cached width calculation state
|
|
||||||
pub fn update_width_calculation_state(&mut self, ctx: &egui::Context, width: f32) {
|
|
||||||
self.cached_width = Some(width);
|
|
||||||
self.last_word_wrap = self.word_wrap;
|
|
||||||
self.last_show_line_numbers = self.show_line_numbers;
|
|
||||||
self.last_font_size = self.font_size;
|
|
||||||
self.last_line_side = self.line_side;
|
|
||||||
self.last_viewport_width = ctx.available_rect().width();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get cached width if available, otherwise return None to indicate calculation is needed
|
|
||||||
pub fn get_cached_width(&self) -> Option<f32> {
|
|
||||||
self.cached_width
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ pub struct Tab {
|
|||||||
pub file_path: Option<PathBuf>,
|
pub file_path: Option<PathBuf>,
|
||||||
pub is_modified: bool,
|
pub is_modified: bool,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
hasher: DefaultHasher,
|
pub hasher: DefaultHasher,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tab {
|
impl Tab {
|
||||||
@ -29,7 +29,7 @@ impl Tab {
|
|||||||
content,
|
content,
|
||||||
file_path: None,
|
file_path: None,
|
||||||
is_modified: false,
|
is_modified: false,
|
||||||
title: format!("new_{}", tab_number),
|
title: format!("new_{tab_number}"),
|
||||||
hasher,
|
hasher,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
use crate::app::tab::Tab;
|
|
||||||
use crate::app::TextEditor;
|
use crate::app::TextEditor;
|
||||||
|
use crate::app::tab::Tab;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ pub(crate) fn open_file(app: &mut TextEditor) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Failed to open file: {}", err);
|
eprintln!("Failed to open file: {err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -81,7 +81,7 @@ pub(crate) fn save_to_path(app: &mut TextEditor, path: PathBuf) {
|
|||||||
active_tab.mark_as_saved();
|
active_tab.mark_as_saved();
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Failed to save file: {}", err);
|
eprintln!("Failed to save file: {err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,15 +11,15 @@ fn main() -> eframe::Result {
|
|||||||
let options = eframe::NativeOptions {
|
let options = eframe::NativeOptions {
|
||||||
viewport: egui::ViewportBuilder::default()
|
viewport: egui::ViewportBuilder::default()
|
||||||
.with_min_inner_size([600.0, 400.0])
|
.with_min_inner_size([600.0, 400.0])
|
||||||
.with_title("C-Ext")
|
.with_title("ced")
|
||||||
.with_app_id("io.lampnet.c-ext"),
|
.with_app_id("io.lampnet.ced"),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let config = Config::load();
|
let config = Config::load();
|
||||||
|
|
||||||
eframe::run_native(
|
eframe::run_native(
|
||||||
"C-Ext",
|
"ced",
|
||||||
options,
|
options,
|
||||||
Box::new(move |cc| Ok(Box::new(TextEditor::from_config_with_context(config, cc)))),
|
Box::new(move |cc| Ok(Box::new(TextEditor::from_config_with_context(config, cc)))),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -14,7 +14,7 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
let line_side = app.line_side;
|
let line_side = app.line_side;
|
||||||
let font_size = app.font_size;
|
let font_size = app.font_size;
|
||||||
|
|
||||||
egui::CentralPanel::default()
|
let output = egui::CentralPanel::default()
|
||||||
.frame(egui::Frame::NONE)
|
.frame(egui::Frame::NONE)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
let bg_color = ui.visuals().extreme_bg_color;
|
let bg_color = ui.visuals().extreme_bg_color;
|
||||||
@ -109,4 +109,55 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
output.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.close_menu();
|
||||||
|
}
|
||||||
|
if ui.button("Copy").clicked() {
|
||||||
|
ui.ctx().send_viewport_cmd(egui::ViewportCommand::RequestCopy);
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
if ui.button("Paste").clicked() {
|
||||||
|
ui.ctx()
|
||||||
|
.send_viewport_cmd(egui::ViewportCommand::RequestPaste);
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
if ui.button("Delete").clicked() {
|
||||||
|
ui.ctx().input_mut(|i| {
|
||||||
|
i.events.push(egui::Event::Key {
|
||||||
|
key: egui::Key::Delete,
|
||||||
|
physical_key: None,
|
||||||
|
pressed: true,
|
||||||
|
repeat: false,
|
||||||
|
modifiers: egui::Modifiers::NONE,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
if ui.button("Select All").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) {
|
||||||
|
let select_all_range = egui::text::CCursorRange::two(
|
||||||
|
egui::text::CCursor::new(0),
|
||||||
|
egui::text::CCursor::new(text_len),
|
||||||
|
);
|
||||||
|
state.cursor.set_char_range(Some(select_all_range));
|
||||||
|
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
||||||
|
}
|
||||||
|
ui.close_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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,28 +1,21 @@
|
|||||||
use crate::app::TextEditor;
|
use crate::app::TextEditor;
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
|
|
||||||
use super::find_highlight::draw_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();
|
||||||
pub(super) fn editor_view(
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
app: &mut TextEditor,
|
|
||||||
) -> (egui::Response, Option<egui::Rect>) {
|
|
||||||
let current_match_position = app.get_current_match_position();
|
|
||||||
let show_find = app.show_find;
|
let show_find = app.show_find;
|
||||||
let prev_show_find = app.prev_show_find;
|
let _prev_show_find = app.prev_show_find;
|
||||||
let show_preferences = app.show_preferences;
|
let show_preferences = app.show_preferences;
|
||||||
let show_about = app.show_about;
|
let show_about = app.show_about;
|
||||||
let show_shortcuts = app.show_shortcuts;
|
let show_shortcuts = app.show_shortcuts;
|
||||||
let word_wrap = app.word_wrap;
|
let word_wrap = app.word_wrap;
|
||||||
let font_size = app.font_size;
|
let font_size = app.font_size;
|
||||||
|
|
||||||
// Check if reset zoom was requested in previous frame
|
|
||||||
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.ctx().memory_mut(|mem| {
|
let should_reset_zoom = ui
|
||||||
mem.data.get_temp::<bool>(reset_zoom_key).unwrap_or(false)
|
.ctx()
|
||||||
});
|
.memory_mut(|mem| mem.data.get_temp::<bool>(reset_zoom_key).unwrap_or(false));
|
||||||
|
|
||||||
// Reset zoom if requested
|
|
||||||
if should_reset_zoom {
|
if should_reset_zoom {
|
||||||
app.zoom_factor = 1.0;
|
app.zoom_factor = 1.0;
|
||||||
ui.ctx().set_zoom_factor(1.0);
|
ui.ctx().set_zoom_factor(1.0);
|
||||||
@ -31,7 +24,16 @@ pub(super) fn editor_view(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(active_tab) = app.get_active_tab_mut() {
|
let estimated_width = if !word_wrap {
|
||||||
|
app.calculate_content_based_width(ui)
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(active_tab) = app.get_active_tab_mut() else {
|
||||||
|
return ui.label("No file open, how did you get here?");
|
||||||
|
};
|
||||||
|
|
||||||
let bg_color = ui.visuals().extreme_bg_color;
|
let bg_color = ui.visuals().extreme_bg_color;
|
||||||
let editor_rect = ui.available_rect_before_wrap();
|
let editor_rect = ui.available_rect_before_wrap();
|
||||||
ui.painter().rect_filled(editor_rect, 0.0, bg_color);
|
ui.painter().rect_filled(editor_rect, 0.0, bg_color);
|
||||||
@ -52,64 +54,81 @@ pub(super) fn editor_view(
|
|||||||
.cursor_at_end(false)
|
.cursor_at_end(false)
|
||||||
.id(egui::Id::new("main_text_editor"));
|
.id(egui::Id::new("main_text_editor"));
|
||||||
|
|
||||||
let output = text_edit.show(ui);
|
let output = if word_wrap {
|
||||||
|
text_edit.show(ui)
|
||||||
// Store text length for context menu
|
} else {
|
||||||
let text_len = active_tab.content.len();
|
egui::ScrollArea::horizontal()
|
||||||
|
.auto_shrink([false; 2])
|
||||||
// Right-click context menu
|
.show(ui, |ui| {
|
||||||
output.response.context_menu(|ui| {
|
ui.allocate_ui_with_layout(
|
||||||
if ui.button("Cut").clicked() {
|
egui::Vec2::new(estimated_width, ui.available_height()),
|
||||||
ui.ctx().input_mut(|i| i.events.push(egui::Event::Cut));
|
egui::Layout::left_to_right(egui::Align::TOP),
|
||||||
ui.close_menu();
|
|ui| text_edit.show(ui),
|
||||||
}
|
)
|
||||||
if ui.button("Copy").clicked() {
|
|
||||||
ui.ctx().input_mut(|i| i.events.push(egui::Event::Copy));
|
|
||||||
ui.close_menu();
|
|
||||||
}
|
|
||||||
if ui.button("Paste").clicked() {
|
|
||||||
ui.ctx()
|
|
||||||
.send_viewport_cmd(egui::ViewportCommand::RequestPaste);
|
|
||||||
ui.close_menu();
|
|
||||||
}
|
|
||||||
if ui.button("Delete").clicked() {
|
|
||||||
ui.ctx().input_mut(|i| {
|
|
||||||
i.events.push(egui::Event::Key {
|
|
||||||
key: egui::Key::Delete,
|
|
||||||
physical_key: None,
|
|
||||||
pressed: true,
|
|
||||||
repeat: false,
|
|
||||||
modifiers: egui::Modifiers::NONE,
|
|
||||||
})
|
})
|
||||||
});
|
.inner
|
||||||
ui.close_menu();
|
.inner
|
||||||
}
|
};
|
||||||
if ui.button("Select All").clicked() {
|
|
||||||
let text_edit_id = egui::Id::new("main_text_editor");
|
let content_changed = output.response.changed();
|
||||||
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
let content_for_processing = if content_changed {
|
||||||
let select_all_range = egui::text::CCursorRange::two(
|
active_tab.update_modified_state();
|
||||||
egui::text::CCursor::new(0),
|
Some(active_tab.content.clone())
|
||||||
egui::text::CCursor::new(text_len),
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let current_cursor_pos = output
|
||||||
|
.state
|
||||||
|
.cursor
|
||||||
|
.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;
|
||||||
|
|
||||||
|
if !previous_content.is_empty() {
|
||||||
|
if let (Some(prev_cursor_pos), Some(curr_cursor_pos)) =
|
||||||
|
(previous_cursor_pos, current_cursor_pos)
|
||||||
|
{
|
||||||
|
app.process_incremental_change(
|
||||||
|
&previous_content,
|
||||||
|
&content,
|
||||||
|
prev_cursor_pos,
|
||||||
|
curr_cursor_pos,
|
||||||
|
ui,
|
||||||
);
|
);
|
||||||
state.cursor.set_char_range(Some(select_all_range));
|
} else {
|
||||||
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
app.process_text_for_rendering(&content, ui);
|
||||||
}
|
|
||||||
ui.close_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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let cursor_rect = if let Some(cursor_range) = output.state.cursor.char_range() {
|
if let Some(cursor_pos) = current_cursor_pos {
|
||||||
let cursor_pos = cursor_range.primary.index;
|
app.current_cursor_line = content[..cursor_pos]
|
||||||
|
.bytes()
|
||||||
|
.filter(|&b| b == b'\n')
|
||||||
|
.count();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.previous_content = content.clone();
|
||||||
|
app.previous_cursor_char_index = current_cursor_pos;
|
||||||
|
|
||||||
|
if let Some(active_tab) = app.get_active_tab_mut() {
|
||||||
|
active_tab.last_content_hash =
|
||||||
|
crate::app::tab::compute_content_hash(&active_tab.content, &mut active_tab.hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !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 cursor_moved || text_changed {
|
||||||
|
if let Some(active_tab) = app.get_active_tab() {
|
||||||
let content = &active_tab.content;
|
let content = &active_tab.content;
|
||||||
|
|
||||||
// Count newlines up to cursor position using char_indices to avoid char boundary issues
|
|
||||||
let cursor_line = content
|
let cursor_line = content
|
||||||
.char_indices()
|
.char_indices()
|
||||||
.take_while(|(byte_pos, _)| *byte_pos < cursor_pos)
|
.take_while(|(byte_pos, _)| *byte_pos < cursor_pos)
|
||||||
@ -129,44 +148,19 @@ pub(super) fn editor_view(
|
|||||||
egui::pos2(output.response.rect.left(), y_pos),
|
egui::pos2(output.response.rect.left(), y_pos),
|
||||||
egui::vec2(2.0, line_height),
|
egui::vec2(2.0, line_height),
|
||||||
);
|
);
|
||||||
Some(cursor_rect)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if !show_find && prev_show_find {
|
let visible_area = ui.clip_rect();
|
||||||
if let Some((start_pos, end_pos)) = current_match_position {
|
if !visible_area.intersects(cursor_rect) {
|
||||||
let text_edit_id = egui::Id::new("main_text_editor");
|
ui.scroll_to_rect(cursor_rect, Some(egui::Align::Center));
|
||||||
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
|
||||||
let cursor_range = egui::text::CCursorRange::two(
|
|
||||||
egui::text::CCursor::new(start_pos),
|
|
||||||
egui::text::CCursor::new(end_pos),
|
|
||||||
);
|
|
||||||
state.cursor.set_char_range(Some(cursor_range));
|
|
||||||
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if show_find {
|
app.previous_cursor_position = Some(cursor_pos);
|
||||||
if let Some((start_pos, end_pos)) = current_match_position {
|
|
||||||
draw_find_highlight(
|
|
||||||
ui,
|
|
||||||
&active_tab.content,
|
|
||||||
start_pos,
|
|
||||||
end_pos,
|
|
||||||
output.response.rect,
|
|
||||||
font_size,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if output.response.changed() {
|
// Request focus if no dialogs are open
|
||||||
active_tab.update_modified_state();
|
|
||||||
app.find_matches.clear();
|
|
||||||
app.current_match_index = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !output.response.has_focus()
|
if !output.response.has_focus()
|
||||||
&& !show_preferences
|
&& !show_preferences
|
||||||
&& !show_about
|
&& !show_about
|
||||||
@ -175,52 +169,6 @@ pub(super) fn editor_view(
|
|||||||
{
|
{
|
||||||
output.response.request_focus();
|
output.response.request_focus();
|
||||||
}
|
}
|
||||||
(output.response, cursor_rect)
|
|
||||||
} else {
|
output.response
|
||||||
(ui.label("No file open, how did you get here?"), None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) {
|
|
||||||
let word_wrap = app.word_wrap;
|
|
||||||
|
|
||||||
if word_wrap {
|
|
||||||
let (_response, _cursor_rect) = editor_view(ui, app);
|
|
||||||
} else {
|
|
||||||
let estimated_width = app.calculate_content_based_width(ui);
|
|
||||||
let output = egui::ScrollArea::horizontal()
|
|
||||||
.auto_shrink([false; 2])
|
|
||||||
.show(ui, |ui| {
|
|
||||||
ui.allocate_ui_with_layout(
|
|
||||||
egui::Vec2::new(estimated_width, ui.available_height()),
|
|
||||||
egui::Layout::left_to_right(egui::Align::TOP),
|
|
||||||
|ui| editor_view(ui, app),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let editor_response = &output.inner.inner.0;
|
|
||||||
if let Some(cursor_rect) = output.inner.inner.1 {
|
|
||||||
let text_edit_id = egui::Id::new("main_text_editor");
|
|
||||||
let current_cursor_pos =
|
|
||||||
if let Some(state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
|
||||||
state.cursor.char_range().map(|range| range.primary.index)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let cursor_moved = current_cursor_pos != app.previous_cursor_position;
|
|
||||||
let text_changed = editor_response.changed();
|
|
||||||
let should_scroll = (cursor_moved || text_changed)
|
|
||||||
&& {
|
|
||||||
let visible_area = ui.clip_rect();
|
|
||||||
!visible_area.intersects(cursor_rect)
|
|
||||||
};
|
|
||||||
|
|
||||||
if should_scroll {
|
|
||||||
ui.scroll_to_rect(cursor_rect, Some(egui::Align::Center));
|
|
||||||
}
|
|
||||||
|
|
||||||
app.previous_cursor_position = current_cursor_pos;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,12 +11,12 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
|
|
||||||
let should_show_menubar = !app.auto_hide_toolbar || {
|
let should_show_menubar = !app.auto_hide_toolbar || {
|
||||||
if app.menu_interaction_active {
|
if app.menu_interaction_active {
|
||||||
app.menu_bar_stable_until = Some(now + std::time::Duration::from_millis(500));
|
app.menu_bar_stable_until = Some(now + std::time::Duration::from_millis(16));
|
||||||
true
|
true
|
||||||
} else if should_stay_stable {
|
} else if should_stay_stable {
|
||||||
true
|
true
|
||||||
} else if let Some(pointer_pos) = ctx.pointer_hover_pos() {
|
} else if let Some(pointer_pos) = ctx.pointer_hover_pos() {
|
||||||
let in_menu_trigger_area = pointer_pos.y < 10.0;
|
let in_menu_trigger_area = pointer_pos.y < 5.0;
|
||||||
|
|
||||||
if in_menu_trigger_area {
|
if in_menu_trigger_area {
|
||||||
app.menu_bar_stable_until = Some(now + std::time::Duration::from_millis(300));
|
app.menu_bar_stable_until = Some(now + std::time::Duration::from_millis(300));
|
||||||
@ -192,6 +192,13 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
app.save_config();
|
app.save_config();
|
||||||
ui.close_menu();
|
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();
|
ui.separator();
|
||||||
|
|
||||||
@ -265,6 +272,41 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
ui.close_menu();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if app.auto_hide_tab_bar {
|
||||||
|
let tab_title = if let Some(tab) = app.get_active_tab() {
|
||||||
|
tab.title.clone()
|
||||||
|
} else {
|
||||||
|
let empty_tab = crate::app::tab::Tab::new_empty(1);
|
||||||
|
empty_tab.title.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let window_width = ctx.screen_rect().width();
|
||||||
|
|
||||||
|
let text_galley = ui.fonts(|fonts| {
|
||||||
|
fonts.layout_job(egui::text::LayoutJob::simple_singleline(
|
||||||
|
tab_title.clone(),
|
||||||
|
app.get_font_id(),
|
||||||
|
ui.style().visuals.text_color(),
|
||||||
|
))
|
||||||
|
});
|
||||||
|
|
||||||
|
let text_width = text_galley.size().x;
|
||||||
|
let text_height = text_galley.size().y;
|
||||||
|
|
||||||
|
let window_center_x = window_width / 2.0;
|
||||||
|
let text_x = (window_center_x - text_width / 2.0).max(0.0);
|
||||||
|
|
||||||
|
let cursor_pos = ui.cursor().left_top();
|
||||||
|
|
||||||
|
ui.painter().galley(
|
||||||
|
egui::pos2(text_x, cursor_pos.y),
|
||||||
|
text_galley,
|
||||||
|
ui.style().visuals.text_color(),
|
||||||
|
);
|
||||||
|
|
||||||
|
ui.allocate_exact_size(egui::vec2(0.0, text_height), egui::Sense::hover());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,9 +27,7 @@ fn render_shortcuts_content(ui: &mut egui::Ui) {
|
|||||||
ui.separator();
|
ui.separator();
|
||||||
ui.label(egui::RichText::new("Views").size(18.0).strong());
|
ui.label(egui::RichText::new("Views").size(18.0).strong());
|
||||||
ui.label(egui::RichText::new("Ctrl + L: Toggle Line Numbers").size(14.0));
|
ui.label(egui::RichText::new("Ctrl + L: Toggle Line Numbers").size(14.0));
|
||||||
ui.label(
|
ui.label(egui::RichText::new("Ctrl + Shift + L: Change Line Number Side").size(14.0));
|
||||||
egui::RichText::new("Ctrl + Shift + L: Change Line Number Side").size(14.0),
|
|
||||||
);
|
|
||||||
ui.label(egui::RichText::new("Ctrl + K: Toggle Word Wrap").size(14.0));
|
ui.label(egui::RichText::new("Ctrl + K: Toggle Word Wrap").size(14.0));
|
||||||
ui.label(egui::RichText::new("Ctrl + H: Toggle Auto Hide Toolbar").size(14.0));
|
ui.label(egui::RichText::new("Ctrl + H: Toggle Auto Hide Toolbar").size(14.0));
|
||||||
ui.label(egui::RichText::new("Ctrl + P: Preferences").size(14.0));
|
ui.label(egui::RichText::new("Ctrl + P: Preferences").size(14.0));
|
||||||
@ -53,8 +51,8 @@ pub(crate) fn shortcuts_window(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
let screen_rect = ctx.screen_rect();
|
let screen_rect = ctx.screen_rect();
|
||||||
|
|
||||||
// Calculate appropriate window size that always fits nicely in the main window
|
// Calculate appropriate window size that always fits nicely in the main window
|
||||||
let window_width = (screen_rect.width() * 0.6).min(400.0).max(300.0);
|
let window_width = (screen_rect.width() * 0.6).clamp(300.0, 400.0);
|
||||||
let window_height = (screen_rect.height() * 0.7).min(500.0).max(250.0);
|
let window_height = (screen_rect.height() * 0.7).clamp(250.0, 500.0);
|
||||||
|
|
||||||
egui::Window::new("Shortcuts")
|
egui::Window::new("Shortcuts")
|
||||||
.collapsible(false)
|
.collapsible(false)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user