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)]
|
||||
pub struct Config {
|
||||
pub auto_hide_toolbar: bool,
|
||||
pub auto_hide_tab_bar: bool,
|
||||
pub show_line_numbers: bool,
|
||||
pub word_wrap: bool,
|
||||
pub theme: Theme,
|
||||
@ -19,6 +20,7 @@ impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
auto_hide_toolbar: false,
|
||||
auto_hide_tab_bar: false,
|
||||
show_line_numbers: false,
|
||||
word_wrap: true,
|
||||
theme: Theme::default(),
|
||||
@ -52,7 +54,7 @@ impl Config {
|
||||
if !config_path.exists() {
|
||||
let default_config = Self::default();
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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::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;
|
||||
|
||||
impl eframe::App for TextEditor {
|
||||
@ -24,77 +24,8 @@ impl eframe::App for TextEditor {
|
||||
|
||||
menu_bar(self, ctx);
|
||||
|
||||
// if self.tabs.len() > 1 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
if !self.auto_hide_tab_bar {
|
||||
tab_bar(self, ctx);
|
||||
}
|
||||
|
||||
central_panel(self, ctx);
|
||||
@ -117,6 +48,5 @@ impl eframe::App for TextEditor {
|
||||
|
||||
// Update the previous find state for next frame
|
||||
self.prev_show_find = self.show_find;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ impl TextEditor {
|
||||
show_line_numbers: config.show_line_numbers,
|
||||
word_wrap: config.word_wrap,
|
||||
auto_hide_toolbar: config.auto_hide_toolbar,
|
||||
auto_hide_tab_bar: config.auto_hide_tab_bar,
|
||||
theme: config.theme,
|
||||
line_side: config.line_side,
|
||||
font_family: config.font_family,
|
||||
@ -35,17 +36,16 @@ impl TextEditor {
|
||||
current_match_index: None,
|
||||
case_sensitive_search: 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,
|
||||
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,8 +72,6 @@ impl TextEditor {
|
||||
|
||||
editor.apply_font_settings(&cc.egui_ctx);
|
||||
|
||||
editor.start_text_processing_thread();
|
||||
|
||||
editor
|
||||
}
|
||||
|
||||
@ -81,6 +79,7 @@ impl TextEditor {
|
||||
Config {
|
||||
auto_hide_toolbar: self.auto_hide_toolbar,
|
||||
show_line_numbers: self.show_line_numbers,
|
||||
auto_hide_tab_bar: self.auto_hide_tab_bar,
|
||||
word_wrap: self.word_wrap,
|
||||
theme: self.theme,
|
||||
line_side: self.line_side,
|
||||
@ -93,7 +92,7 @@ impl TextEditor {
|
||||
pub fn save_config(&self) {
|
||||
let config = self.get_config();
|
||||
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,
|
||||
word_wrap: true,
|
||||
auto_hide_toolbar: false,
|
||||
auto_hide_tab_bar: false,
|
||||
theme: Theme::default(),
|
||||
line_side: false,
|
||||
font_family: "Proportional".to_string(),
|
||||
@ -36,17 +37,15 @@ impl Default for TextEditor {
|
||||
current_match_index: None,
|
||||
case_sensitive_search: 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
|
||||
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)]
|
||||
pub struct TextProcessingResult {
|
||||
pub line_count: usize,
|
||||
pub visual_line_mapping: Vec<Option<usize>>,
|
||||
pub max_line_length: f32,
|
||||
pub _processed_content: String,
|
||||
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
|
||||
}
|
||||
|
||||
impl Default for TextProcessingResult {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
line_count: 1,
|
||||
visual_line_mapping: vec![Some(1)],
|
||||
max_line_length: 0.0,
|
||||
_processed_content: String::new(),
|
||||
longest_line_index: 0,
|
||||
longest_line_length: 0,
|
||||
longest_line_pixel_width: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -44,6 +44,7 @@ pub struct TextEditor {
|
||||
pub(crate) show_line_numbers: bool,
|
||||
pub(crate) word_wrap: bool,
|
||||
pub(crate) auto_hide_toolbar: bool,
|
||||
pub(crate) auto_hide_tab_bar: bool,
|
||||
pub(crate) theme: Theme,
|
||||
pub(crate) line_side: bool,
|
||||
pub(crate) font_family: String,
|
||||
@ -61,15 +62,12 @@ pub struct TextEditor {
|
||||
pub(crate) case_sensitive_search: bool,
|
||||
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
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
@ -1,63 +1,282 @@
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
use super::editor::{TextEditor, TextProcessingResult};
|
||||
use eframe::egui;
|
||||
|
||||
impl TextEditor {
|
||||
pub fn start_text_processing_thread(&mut self) {
|
||||
let _processing_result = Arc::clone(&self.text_processing_result);
|
||||
|
||||
let handle = thread::Builder::new()
|
||||
.name("TextProcessor".to_string())
|
||||
.spawn(move || {
|
||||
// Set thread priority to high (platform-specific)
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
/// 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);
|
||||
|
||||
if lines.is_empty() {
|
||||
self.update_processing_result(TextProcessingResult {
|
||||
line_count: 1,
|
||||
longest_line_index: 0,
|
||||
longest_line_length: 0,
|
||||
longest_line_pixel_width: 0.0,
|
||||
});
|
||||
|
||||
match handle {
|
||||
Ok(h) => self.processing_thread_handle = Some(h),
|
||||
Err(e) => eprintln!("Failed to start text processing thread: {}", e),
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_text_for_rendering(
|
||||
&mut self,
|
||||
content: &str,
|
||||
available_width: f32,
|
||||
) {
|
||||
let line_count = content.lines().count().max(1);
|
||||
// Find the longest line by character count first (fast)
|
||||
let mut longest_line_index = 0;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
let visual_line_mapping = if self.word_wrap {
|
||||
// For now, simplified mapping - this could be moved to background thread
|
||||
(1..=line_count).map(Some).collect()
|
||||
// Calculate pixel width for the longest line
|
||||
let font_id = self.get_font_id();
|
||||
let longest_line_pixel_width = if longest_line_length > 0 {
|
||||
let longest_line_text = lines[longest_line_index];
|
||||
ui.fonts(|fonts| {
|
||||
fonts.layout(
|
||||
longest_line_text.to_string(),
|
||||
font_id,
|
||||
egui::Color32::WHITE,
|
||||
f32::INFINITY,
|
||||
).size().x
|
||||
})
|
||||
} else {
|
||||
(1..=line_count).map(Some).collect()
|
||||
0.0
|
||||
};
|
||||
|
||||
let result = TextProcessingResult {
|
||||
line_count,
|
||||
visual_line_mapping,
|
||||
max_line_length: available_width,
|
||||
_processed_content: content.to_string(),
|
||||
longest_line_index,
|
||||
longest_line_length,
|
||||
longest_line_pixel_width,
|
||||
};
|
||||
|
||||
if let Ok(mut processing_result) = self.text_processing_result.lock() {
|
||||
*processing_result = result;
|
||||
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, 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 {
|
||||
self.text_processing_result
|
||||
.lock()
|
||||
.map(|result| result.clone())
|
||||
.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 {
|
||||
if let Some(tab) = self.get_active_tab() {
|
||||
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 {
|
||||
"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
|
||||
fn get_font_id(&self) -> egui::FontId {
|
||||
pub fn get_font_id(&self) -> egui::FontId {
|
||||
let font_family = match self.font_family.as_str() {
|
||||
"Monospace" => egui::FontFamily::Monospace,
|
||||
_ => egui::FontFamily::Proportional,
|
||||
@ -51,7 +56,7 @@ impl TextEditor {
|
||||
/// Calculates the available width for the text editor, accounting for line numbers and separator
|
||||
pub fn calculate_editor_dimensions(&self, ui: &egui::Ui) -> EditorDimensions {
|
||||
let total_available_width = ui.available_width();
|
||||
|
||||
|
||||
if !self.show_line_numbers {
|
||||
return EditorDimensions {
|
||||
text_width: total_available_width,
|
||||
@ -63,7 +68,7 @@ impl TextEditor {
|
||||
// Get line count from processing result
|
||||
let processing_result = self.get_text_processing_result();
|
||||
let line_count = processing_result.line_count;
|
||||
|
||||
|
||||
// Calculate base line number width
|
||||
let font_id = self.get_font_id();
|
||||
let line_count_digits = line_count.to_string().len();
|
||||
@ -77,14 +82,14 @@ impl TextEditor {
|
||||
|
||||
// Add padding based on line_side setting
|
||||
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 {
|
||||
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)
|
||||
let separator_width = 10.0;
|
||||
|
||||
|
||||
let total_reserved_width = line_number_width + separator_width;
|
||||
let text_width = (total_available_width - total_reserved_width).max(100.0); // Minimum 100px for text
|
||||
|
||||
@ -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 {
|
||||
if let Some(active_tab) = self.get_active_tab() {
|
||||
let content = &active_tab.content;
|
||||
|
||||
if content.is_empty() {
|
||||
return self.calculate_editor_dimensions(ui).text_width;
|
||||
}
|
||||
let processing_result = self.get_text_processing_result();
|
||||
|
||||
// Find the longest line
|
||||
let longest_line = content
|
||||
.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
|
||||
let dimensions = self.calculate_editor_dimensions(ui);
|
||||
longest_line_width.max(dimensions.text_width)
|
||||
} else {
|
||||
self.calculate_editor_dimensions(ui).text_width
|
||||
if processing_result.longest_line_length == 0 {
|
||||
return 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
|
||||
}
|
||||
// Use the pre-calculated pixel width with some padding
|
||||
let longest_line_width = processing_result.longest_line_pixel_width + 20.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
|
||||
// Return the larger of the calculated width or minimum available width
|
||||
let dimensions = self.calculate_editor_dimensions(ui);
|
||||
longest_line_width.max(dimensions.text_width)
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ pub struct Tab {
|
||||
pub file_path: Option<PathBuf>,
|
||||
pub is_modified: bool,
|
||||
pub title: String,
|
||||
hasher: DefaultHasher,
|
||||
pub hasher: DefaultHasher,
|
||||
}
|
||||
|
||||
impl Tab {
|
||||
@ -29,7 +29,7 @@ impl Tab {
|
||||
content,
|
||||
file_path: None,
|
||||
is_modified: false,
|
||||
title: format!("new_{}", tab_number),
|
||||
title: format!("new_{tab_number}"),
|
||||
hasher,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
use crate::app::tab::Tab;
|
||||
use crate::app::TextEditor;
|
||||
use crate::app::tab::Tab;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@ -43,7 +43,7 @@ pub(crate) fn open_file(app: &mut TextEditor) {
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
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 {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_min_inner_size([600.0, 400.0])
|
||||
.with_title("C-Ext")
|
||||
.with_app_id("io.lampnet.c-ext"),
|
||||
.with_title("ced")
|
||||
.with_app_id("io.lampnet.ced"),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let config = Config::load();
|
||||
|
||||
eframe::run_native(
|
||||
"C-Ext",
|
||||
"ced",
|
||||
options,
|
||||
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 font_size = app.font_size;
|
||||
|
||||
egui::CentralPanel::default()
|
||||
let output = egui::CentralPanel::default()
|
||||
.frame(egui::Frame::NONE)
|
||||
.show(ctx, |ui| {
|
||||
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 eframe::egui;
|
||||
|
||||
use super::find_highlight::draw_find_highlight;
|
||||
|
||||
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();
|
||||
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;
|
||||
let prev_show_find = app.prev_show_find;
|
||||
let _prev_show_find = app.prev_show_find;
|
||||
let show_preferences = app.show_preferences;
|
||||
let show_about = app.show_about;
|
||||
let show_shortcuts = app.show_shortcuts;
|
||||
let word_wrap = app.word_wrap;
|
||||
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 should_reset_zoom = ui.ctx().memory_mut(|mem| {
|
||||
mem.data.get_temp::<bool>(reset_zoom_key).unwrap_or(false)
|
||||
});
|
||||
|
||||
// Reset zoom if requested
|
||||
let should_reset_zoom = ui
|
||||
.ctx()
|
||||
.memory_mut(|mem| mem.data.get_temp::<bool>(reset_zoom_key).unwrap_or(false));
|
||||
|
||||
if should_reset_zoom {
|
||||
app.zoom_factor = 1.0;
|
||||
ui.ctx().set_zoom_factor(1.0);
|
||||
@ -31,196 +24,151 @@ pub(super) fn editor_view(
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(active_tab) = app.get_active_tab_mut() {
|
||||
let bg_color = ui.visuals().extreme_bg_color;
|
||||
let editor_rect = ui.available_rect_before_wrap();
|
||||
ui.painter().rect_filled(editor_rect, 0.0, bg_color);
|
||||
|
||||
let desired_width = if word_wrap {
|
||||
ui.available_width()
|
||||
} else {
|
||||
f32::INFINITY
|
||||
};
|
||||
|
||||
let text_edit = egui::TextEdit::multiline(&mut active_tab.content)
|
||||
.frame(false)
|
||||
.font(egui::TextStyle::Monospace)
|
||||
.code_editor()
|
||||
.desired_width(desired_width)
|
||||
.desired_rows(0)
|
||||
.lock_focus(true)
|
||||
.cursor_at_end(false)
|
||||
.id(egui::Id::new("main_text_editor"));
|
||||
|
||||
let output = text_edit.show(ui);
|
||||
|
||||
// Store text length for context menu
|
||||
let text_len = active_tab.content.len();
|
||||
|
||||
// Right-click context menu
|
||||
output.response.context_menu(|ui| {
|
||||
if ui.button("Cut").clicked() {
|
||||
ui.ctx().input_mut(|i| i.events.push(egui::Event::Cut));
|
||||
ui.close_menu();
|
||||
}
|
||||
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,
|
||||
})
|
||||
});
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
let cursor_rect = if let Some(cursor_range) = output.state.cursor.char_range() {
|
||||
let cursor_pos = cursor_range.primary.index;
|
||||
let content = &active_tab.content;
|
||||
|
||||
// Count newlines up to cursor position using char_indices to avoid char boundary issues
|
||||
let cursor_line = content
|
||||
.char_indices()
|
||||
.take_while(|(byte_pos, _)| *byte_pos < cursor_pos)
|
||||
.filter(|(_, ch)| *ch == '\n')
|
||||
.count();
|
||||
|
||||
let font_id = ui
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&egui::TextStyle::Monospace)
|
||||
.unwrap_or(&egui::FontId::monospace(font_size))
|
||||
.clone();
|
||||
let line_height = ui.fonts(|fonts| fonts.row_height(&font_id));
|
||||
|
||||
let y_pos = output.response.rect.top() + (cursor_line as f32 * line_height);
|
||||
let cursor_rect = egui::Rect::from_min_size(
|
||||
egui::pos2(output.response.rect.left(), y_pos),
|
||||
egui::vec2(2.0, line_height),
|
||||
);
|
||||
Some(cursor_rect)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if !show_find && prev_show_find {
|
||||
if let Some((start_pos, end_pos)) = current_match_position {
|
||||
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 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 {
|
||||
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() {
|
||||
active_tab.update_modified_state();
|
||||
app.find_matches.clear();
|
||||
app.current_match_index = None;
|
||||
}
|
||||
|
||||
if !output.response.has_focus()
|
||||
&& !show_preferences
|
||||
&& !show_about
|
||||
&& !show_shortcuts
|
||||
&& !show_find
|
||||
{
|
||||
output.response.request_focus();
|
||||
}
|
||||
(output.response, cursor_rect)
|
||||
let estimated_width = if !word_wrap {
|
||||
app.calculate_content_based_width(ui)
|
||||
} else {
|
||||
(ui.label("No file open, how did you get here?"), None)
|
||||
}
|
||||
}
|
||||
0.0
|
||||
};
|
||||
|
||||
pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) {
|
||||
let word_wrap = app.word_wrap;
|
||||
let Some(active_tab) = app.get_active_tab_mut() else {
|
||||
return ui.label("No file open, how did you get here?");
|
||||
};
|
||||
|
||||
if word_wrap {
|
||||
let (_response, _cursor_rect) = editor_view(ui, app);
|
||||
let bg_color = ui.visuals().extreme_bg_color;
|
||||
let editor_rect = ui.available_rect_before_wrap();
|
||||
ui.painter().rect_filled(editor_rect, 0.0, bg_color);
|
||||
|
||||
let desired_width = if word_wrap {
|
||||
ui.available_width()
|
||||
} else {
|
||||
let estimated_width = app.calculate_content_based_width(ui);
|
||||
let output = egui::ScrollArea::horizontal()
|
||||
f32::INFINITY
|
||||
};
|
||||
|
||||
let text_edit = egui::TextEdit::multiline(&mut active_tab.content)
|
||||
.frame(false)
|
||||
.font(egui::TextStyle::Monospace)
|
||||
.code_editor()
|
||||
.desired_width(desired_width)
|
||||
.desired_rows(0)
|
||||
.lock_focus(true)
|
||||
.cursor_at_end(false)
|
||||
.id(egui::Id::new("main_text_editor"));
|
||||
|
||||
let output = if word_wrap {
|
||||
text_edit.show(ui)
|
||||
} else {
|
||||
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),
|
||||
|ui| text_edit.show(ui),
|
||||
)
|
||||
});
|
||||
})
|
||||
.inner
|
||||
.inner
|
||||
};
|
||||
|
||||
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 content_changed = output.response.changed();
|
||||
let content_for_processing = if content_changed {
|
||||
active_tab.update_modified_state();
|
||||
Some(active_tab.content.clone())
|
||||
} 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)
|
||||
};
|
||||
let current_cursor_pos = output
|
||||
.state
|
||||
.cursor
|
||||
.char_range()
|
||||
.map(|range| range.primary.index);
|
||||
|
||||
if should_scroll {
|
||||
ui.scroll_to_rect(cursor_rect, Some(egui::Align::Center));
|
||||
|
||||
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,
|
||||
);
|
||||
} 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.previous_cursor_position = current_cursor_pos;
|
||||
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 cursor_line = content
|
||||
.char_indices()
|
||||
.take_while(|(byte_pos, _)| *byte_pos < cursor_pos)
|
||||
.filter(|(_, ch)| *ch == '\n')
|
||||
.count();
|
||||
|
||||
let font_id = ui
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&egui::TextStyle::Monospace)
|
||||
.unwrap_or(&egui::FontId::monospace(font_size))
|
||||
.clone();
|
||||
let line_height = ui.fonts(|fonts| fonts.row_height(&font_id));
|
||||
|
||||
let y_pos = output.response.rect.top() + (cursor_line as f32 * line_height);
|
||||
let cursor_rect = egui::Rect::from_min_size(
|
||||
egui::pos2(output.response.rect.left(), y_pos),
|
||||
egui::vec2(2.0, line_height),
|
||||
);
|
||||
|
||||
let visible_area = ui.clip_rect();
|
||||
if !visible_area.intersects(cursor_rect) {
|
||||
ui.scroll_to_rect(cursor_rect, Some(egui::Align::Center));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.previous_cursor_position = Some(cursor_pos);
|
||||
}
|
||||
}
|
||||
|
||||
// Request focus if no dialogs are open
|
||||
if !output.response.has_focus()
|
||||
&& !show_preferences
|
||||
&& !show_about
|
||||
&& !show_shortcuts
|
||||
&& !show_find
|
||||
{
|
||||
output.response.request_focus();
|
||||
}
|
||||
|
||||
output.response
|
||||
}
|
||||
|
||||
@ -11,12 +11,12 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
|
||||
let should_show_menubar = !app.auto_hide_toolbar || {
|
||||
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
|
||||
} else if should_stay_stable {
|
||||
true
|
||||
} 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 {
|
||||
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();
|
||||
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();
|
||||
|
||||
@ -265,6 +272,41 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
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.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 + Shift + L: Change Line Number Side").size(14.0),
|
||||
);
|
||||
ui.label(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 + H: Toggle Auto Hide Toolbar").size(14.0));
|
||||
ui.label(egui::RichText::new("Ctrl + P: Preferences").size(14.0));
|
||||
@ -51,10 +49,10 @@ fn render_shortcuts_content(ui: &mut egui::Ui) {
|
||||
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).min(400.0).max(300.0);
|
||||
let window_height = (screen_rect.height() * 0.7).min(500.0).max(250.0);
|
||||
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);
|
||||
|
||||
egui::Window::new("Shortcuts")
|
||||
.collapsible(false)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user