markdown, better wrapping
This commit is contained in:
parent
a3158129d1
commit
d2fb8bf8ed
@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "ced"
|
||||
version = "0.1.3"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
eframe = "0.32"
|
||||
egui = "0.32"
|
||||
egui_extras = { version = "0.32", features = ["syntect"] }
|
||||
eframe = "0.33.3"
|
||||
egui = "0.33.3"
|
||||
egui_extras = { version = "0.33.3", features = ["syntect"] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.141"
|
||||
rfd = "0.15.4"
|
||||
@ -17,3 +17,4 @@ syntect = "5.2.0"
|
||||
plist = "1.7.4"
|
||||
diffy = "0.4.2"
|
||||
uuid = { version = "1.0", features = ["v4"] }
|
||||
egui_commonmark = { version = "0.22" }
|
||||
@ -14,8 +14,10 @@ enum ShortcutAction {
|
||||
ToggleLineSide,
|
||||
ToggleWordWrap,
|
||||
ToggleAutoHideToolbar,
|
||||
ToggleBottomBar,
|
||||
ToggleFind,
|
||||
ToggleReplace,
|
||||
ToggleMarkdown,
|
||||
FocusFind,
|
||||
NextTab,
|
||||
PrevTab,
|
||||
@ -92,6 +94,11 @@ fn get_shortcuts() -> Vec<ShortcutDefinition> {
|
||||
egui::Key::H,
|
||||
ShortcutAction::ToggleAutoHideToolbar,
|
||||
),
|
||||
(
|
||||
egui::Modifiers::CTRL,
|
||||
egui::Key::B,
|
||||
ShortcutAction::ToggleBottomBar,
|
||||
),
|
||||
(
|
||||
egui::Modifiers::CTRL | egui::Modifiers::SHIFT,
|
||||
egui::Key::Tab,
|
||||
@ -152,6 +159,11 @@ fn get_shortcuts() -> Vec<ShortcutDefinition> {
|
||||
egui::Key::Escape,
|
||||
ShortcutAction::Escape,
|
||||
),
|
||||
(
|
||||
egui::Modifiers::CTRL,
|
||||
egui::Key::M,
|
||||
ShortcutAction::ToggleMarkdown,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
@ -211,6 +223,11 @@ fn execute_action(action: ShortcutAction, editor: &mut TextEditor) -> bool {
|
||||
editor.save_config();
|
||||
false
|
||||
}
|
||||
ShortcutAction::ToggleBottomBar => {
|
||||
editor.hide_bottom_bar = !editor.hide_bottom_bar;
|
||||
editor.save_config();
|
||||
false
|
||||
}
|
||||
ShortcutAction::NextTab => {
|
||||
let next_tab_index = editor.active_tab_index + 1;
|
||||
if next_tab_index < editor.tabs.len() {
|
||||
@ -228,8 +245,9 @@ fn execute_action(action: ShortcutAction, editor: &mut TextEditor) -> bool {
|
||||
}
|
||||
false
|
||||
}
|
||||
ShortcutAction::PageUp => false,
|
||||
ShortcutAction::PageDown => false,
|
||||
ShortcutAction::PageUp | ShortcutAction::PageDown => {
|
||||
false
|
||||
}
|
||||
ShortcutAction::ZoomIn => {
|
||||
editor.font_size += 1.0;
|
||||
true
|
||||
@ -293,12 +311,18 @@ fn execute_action(action: ShortcutAction, editor: &mut TextEditor) -> bool {
|
||||
editor.show_preferences = !editor.show_preferences;
|
||||
false
|
||||
}
|
||||
ShortcutAction::ToggleMarkdown => {
|
||||
editor.show_markdown = !editor.show_markdown;
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle(editor: &mut TextEditor, ctx: &egui::Context) {
|
||||
let mut font_zoom_occurred = false;
|
||||
let mut global_zoom_occurred = false;
|
||||
let mut page_up_pressed = false;
|
||||
let mut page_down_pressed = false;
|
||||
|
||||
ctx.input_mut(|i| {
|
||||
for (modifiers, key, action) in get_shortcuts() {
|
||||
@ -313,6 +337,12 @@ pub fn handle(editor: &mut TextEditor, ctx: &egui::Context) {
|
||||
execute_action(action, editor);
|
||||
global_zoom_occurred = true;
|
||||
}
|
||||
ShortcutAction::PageUp => {
|
||||
page_up_pressed = true;
|
||||
}
|
||||
ShortcutAction::PageDown => {
|
||||
page_down_pressed = true;
|
||||
}
|
||||
_ => {
|
||||
execute_action(action, editor);
|
||||
}
|
||||
@ -330,6 +360,14 @@ pub fn handle(editor: &mut TextEditor, ctx: &egui::Context) {
|
||||
ctx.set_zoom_factor(editor.zoom_factor);
|
||||
}
|
||||
|
||||
if page_up_pressed {
|
||||
editor.handle_page_movement(ctx, false);
|
||||
}
|
||||
|
||||
if page_down_pressed {
|
||||
editor.handle_page_movement(ctx, true);
|
||||
}
|
||||
|
||||
if editor.should_select_current_match {
|
||||
editor.select_current_match(ctx);
|
||||
editor.should_select_current_match = false;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use super::editor::TextEditor;
|
||||
use crate::app::shortcuts;
|
||||
use crate::ui::about_window::about_window;
|
||||
use crate::ui::bottom_bar::bottom_bar;
|
||||
use crate::ui::central_panel::central_panel;
|
||||
use crate::ui::find_window::find_window;
|
||||
use crate::ui::menu_bar::menu_bar;
|
||||
@ -28,6 +29,10 @@ impl eframe::App for TextEditor {
|
||||
tab_bar(self, ctx);
|
||||
}
|
||||
|
||||
if !self.hide_bottom_bar {
|
||||
bottom_bar(self, ctx);
|
||||
}
|
||||
|
||||
central_panel(self, ctx);
|
||||
|
||||
if self.show_about {
|
||||
|
||||
@ -14,6 +14,7 @@ impl Default for TextEditor {
|
||||
show_shortcuts: false,
|
||||
show_find: false,
|
||||
show_preferences: false,
|
||||
show_markdown: false,
|
||||
pending_unsaved_action: None,
|
||||
force_quit_confirmed: false,
|
||||
clean_quit_requested: false,
|
||||
@ -21,6 +22,7 @@ impl Default for TextEditor {
|
||||
word_wrap: true,
|
||||
auto_hide_toolbar: false,
|
||||
hide_tab_bar: true,
|
||||
hide_bottom_bar: false,
|
||||
syntax_highlighting: false,
|
||||
theme: Theme::default(),
|
||||
line_side: false,
|
||||
@ -45,6 +47,7 @@ impl Default for TextEditor {
|
||||
previous_content: String::new(),
|
||||
previous_cursor_char_index: None,
|
||||
current_cursor_line: 0,
|
||||
current_cursor_index: 0,
|
||||
previous_cursor_line: 0,
|
||||
font_settings_changed: false,
|
||||
text_needs_processing: false,
|
||||
|
||||
@ -39,6 +39,7 @@ pub struct TextEditor {
|
||||
pub(crate) show_shortcuts: bool,
|
||||
pub(crate) show_find: bool,
|
||||
pub(crate) show_preferences: bool,
|
||||
pub(crate) show_markdown: bool,
|
||||
pub(crate) pending_unsaved_action: Option<UnsavedAction>,
|
||||
pub(crate) force_quit_confirmed: bool,
|
||||
pub(crate) clean_quit_requested: bool,
|
||||
@ -46,6 +47,7 @@ pub struct TextEditor {
|
||||
pub(crate) word_wrap: bool,
|
||||
pub(crate) auto_hide_toolbar: bool,
|
||||
pub(crate) hide_tab_bar: bool,
|
||||
pub(crate) hide_bottom_bar: bool,
|
||||
pub(crate) syntax_highlighting: bool,
|
||||
pub(crate) theme: Theme,
|
||||
pub(crate) line_side: bool,
|
||||
@ -69,6 +71,7 @@ pub struct TextEditor {
|
||||
pub(crate) previous_content: String,
|
||||
pub(crate) previous_cursor_char_index: Option<usize>,
|
||||
pub(crate) current_cursor_line: usize,
|
||||
pub(crate) current_cursor_index: usize,
|
||||
pub(crate) previous_cursor_line: usize,
|
||||
pub(crate) font_settings_changed: bool,
|
||||
pub(crate) text_needs_processing: bool,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
use super::editor::TextEditor;
|
||||
use crate::util::safe_slice_to_pos;
|
||||
use eframe::egui;
|
||||
|
||||
impl TextEditor {
|
||||
@ -114,10 +115,15 @@ impl TextEditor {
|
||||
if let Some(active_tab) = self.get_active_tab() {
|
||||
let content = &active_tab.content;
|
||||
|
||||
let start_char = Self::safe_slice_to_pos(content, start_byte).chars().count();
|
||||
let end_char = Self::safe_slice_to_pos(content, end_byte).chars().count();
|
||||
let start_char = safe_slice_to_pos(content, start_byte).chars().count();
|
||||
let end_char = safe_slice_to_pos(content, end_byte).chars().count();
|
||||
|
||||
let text_edit_id = egui::Id::new("main_text_editor");
|
||||
let id_source = active_tab
|
||||
.file_path
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "untitled".to_string());
|
||||
let text_edit_id = egui::Id::new("main_text_editor").with(&id_source);
|
||||
if let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) {
|
||||
let selection_range = egui::text::CCursorRange::two(
|
||||
egui::text::CCursor::new(start_char),
|
||||
@ -152,12 +158,16 @@ impl TextEditor {
|
||||
self.update_find_matches();
|
||||
|
||||
if let Some(active_tab) = self.get_active_tab() {
|
||||
let replacement_end_char =
|
||||
Self::safe_slice_to_pos(&active_tab.content, replacement_end)
|
||||
let replacement_end_char = safe_slice_to_pos(&active_tab.content, replacement_end)
|
||||
.chars()
|
||||
.count();
|
||||
|
||||
let text_edit_id = egui::Id::new("main_text_editor");
|
||||
let id_source = active_tab
|
||||
.file_path
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "untitled".to_string());
|
||||
let text_edit_id = egui::Id::new("main_text_editor").with(&id_source);
|
||||
if let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) {
|
||||
state
|
||||
.cursor
|
||||
@ -206,7 +216,14 @@ impl TextEditor {
|
||||
|
||||
self.current_match_index = None;
|
||||
|
||||
let text_edit_id = egui::Id::new("main_text_editor");
|
||||
if let Some(active_tab) = self.get_active_tab() {
|
||||
let id_source = active_tab
|
||||
.file_path
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "untitled".to_string());
|
||||
let text_edit_id = egui::Id::new("main_text_editor").with(&id_source);
|
||||
|
||||
if let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) {
|
||||
state
|
||||
.cursor
|
||||
@ -217,3 +234,4 @@ impl TextEditor {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,16 +1,8 @@
|
||||
use super::editor::{TextEditor, TextProcessingResult};
|
||||
use crate::util::safe_slice_to_pos;
|
||||
use eframe::egui;
|
||||
|
||||
impl TextEditor {
|
||||
pub(crate) fn safe_slice_to_pos(content: &str, pos: usize) -> &str {
|
||||
let pos = pos.min(content.len());
|
||||
let mut boundary_pos = pos;
|
||||
while boundary_pos > 0 && !content.is_char_boundary(boundary_pos) {
|
||||
boundary_pos -= 1;
|
||||
}
|
||||
&content[..boundary_pos]
|
||||
}
|
||||
|
||||
pub fn process_text_for_rendering(&mut self, content: &str, ui: &egui::Ui) {
|
||||
let line_count = content.bytes().filter(|&b| b == b'\n').count() + 1;
|
||||
|
||||
@ -50,13 +42,12 @@ impl TextEditor {
|
||||
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| {
|
||||
ui.fonts_mut(|fonts| {
|
||||
fonts
|
||||
.layout(
|
||||
.layout_no_wrap(
|
||||
longest_line_text.to_string(),
|
||||
font_id,
|
||||
egui::Color32::WHITE,
|
||||
f32::INFINITY,
|
||||
)
|
||||
.size()
|
||||
.x
|
||||
@ -83,15 +74,6 @@ impl TextEditor {
|
||||
new_cursor_pos: usize,
|
||||
ui: &egui::Ui,
|
||||
) {
|
||||
let line_change = self.calculate_cursor_line_change(
|
||||
old_content,
|
||||
new_content,
|
||||
old_cursor_pos,
|
||||
new_cursor_pos,
|
||||
);
|
||||
|
||||
self.current_cursor_line = (self.current_cursor_line as isize + line_change) as usize;
|
||||
|
||||
if old_content.len() == new_content.len() {
|
||||
self.handle_character_replacement(
|
||||
old_content,
|
||||
@ -128,12 +110,12 @@ impl TextEditor {
|
||||
old_cursor_pos: usize,
|
||||
new_cursor_pos: usize,
|
||||
) -> isize {
|
||||
let old_newlines = Self::safe_slice_to_pos(old_content, old_cursor_pos)
|
||||
let old_newlines = safe_slice_to_pos(old_content, old_cursor_pos)
|
||||
.bytes()
|
||||
.filter(|&b| b == b'\n')
|
||||
.count();
|
||||
|
||||
let new_newlines = Self::safe_slice_to_pos(new_content, new_cursor_pos)
|
||||
let new_newlines = safe_slice_to_pos(new_content, new_cursor_pos)
|
||||
.bytes()
|
||||
.filter(|&b| b == b'\n')
|
||||
.count();
|
||||
@ -198,11 +180,11 @@ impl TextEditor {
|
||||
let mut current_result = self.get_text_processing_result();
|
||||
current_result.line_count += newlines_added;
|
||||
|
||||
let addition_start_line = Self::safe_slice_to_pos(old_content, added_start)
|
||||
let addition_start_line = safe_slice_to_pos(old_content, added_start)
|
||||
.bytes()
|
||||
.filter(|&b| b == b'\n')
|
||||
.count();
|
||||
let addition_end_line = Self::safe_slice_to_pos(old_content, added_end)
|
||||
let addition_end_line = safe_slice_to_pos(old_content, added_end)
|
||||
.bytes()
|
||||
.filter(|&b| b == b'\n')
|
||||
.count();
|
||||
@ -268,11 +250,11 @@ impl TextEditor {
|
||||
let mut current_result = self.get_text_processing_result();
|
||||
current_result.line_count = current_result.line_count.saturating_sub(newlines_removed);
|
||||
|
||||
let removal_start_line = Self::safe_slice_to_pos(old_content, removed_start)
|
||||
let removal_start_line = safe_slice_to_pos(old_content, removed_start)
|
||||
.bytes()
|
||||
.filter(|&b| b == b'\n')
|
||||
.count();
|
||||
let removal_end_line = Self::safe_slice_to_pos(old_content, removed_end)
|
||||
let removal_end_line = safe_slice_to_pos(old_content, removed_end)
|
||||
.bytes()
|
||||
.filter(|&b| b == b'\n')
|
||||
.count();
|
||||
@ -331,7 +313,7 @@ impl TextEditor {
|
||||
{
|
||||
content[line_start_boundary..line_end_boundary].to_string()
|
||||
} else {
|
||||
Self::safe_slice_to_pos(content, line_end_boundary)[line_start_boundary..].to_string()
|
||||
safe_slice_to_pos(content, line_end_boundary)[line_start_boundary..].to_string()
|
||||
}
|
||||
}
|
||||
|
||||
@ -346,13 +328,12 @@ impl TextEditor {
|
||||
|
||||
if line_length > current_result.longest_line_length {
|
||||
let font_id = self.get_font_id();
|
||||
let pixel_width = ui.fonts(|fonts| {
|
||||
let pixel_width = ui.fonts_mut(|fonts| {
|
||||
fonts
|
||||
.layout(
|
||||
.layout_no_wrap(
|
||||
line_content.to_string(),
|
||||
font_id,
|
||||
egui::Color32::WHITE,
|
||||
f32::INFINITY,
|
||||
)
|
||||
.size()
|
||||
.x
|
||||
|
||||
@ -68,7 +68,7 @@ impl TextEditor {
|
||||
let font_id = self.get_font_id();
|
||||
let line_count_digits = line_count.to_string().len();
|
||||
let sample_text = "9".repeat(line_count_digits);
|
||||
let base_line_number_width = ui.fonts(|fonts| {
|
||||
let base_line_number_width = ui.fonts_mut(|fonts| {
|
||||
fonts
|
||||
.layout(sample_text, font_id, egui::Color32::WHITE, f32::INFINITY)
|
||||
.size()
|
||||
@ -76,7 +76,7 @@ impl TextEditor {
|
||||
});
|
||||
|
||||
let line_number_width = if self.line_side {
|
||||
base_line_number_width + 25.0 // Scrollbar width
|
||||
base_line_number_width + crate::ui::constants::SCROLLBAR_WIDTH
|
||||
} else {
|
||||
base_line_number_width
|
||||
};
|
||||
@ -95,10 +95,123 @@ impl TextEditor {
|
||||
return self.calculate_editor_dimensions(ui).text_width;
|
||||
}
|
||||
|
||||
let longest_line_width =
|
||||
processing_result.longest_line_pixel_width + (self.font_size * 3.0);
|
||||
let longest_line_width = processing_result.longest_line_pixel_width;
|
||||
let font_id = self.get_font_id();
|
||||
let char_width = ui.fonts_mut(|fonts| {
|
||||
fonts
|
||||
.layout_no_wrap("M".to_string(), font_id, egui::Color32::WHITE)
|
||||
.size()
|
||||
.x
|
||||
});
|
||||
let extra_space = char_width * 2.0;
|
||||
|
||||
let dimensions = self.calculate_editor_dimensions(ui);
|
||||
longest_line_width.max(dimensions.text_width)
|
||||
(longest_line_width + extra_space).max(dimensions.text_width)
|
||||
}
|
||||
|
||||
pub fn get_cursor_position(&self) -> (usize, usize) {
|
||||
if let Some(active_tab) = self.get_active_tab() {
|
||||
let content = &active_tab.content;
|
||||
let safe_pos = self.current_cursor_index.min(content.len());
|
||||
|
||||
// Calculate column (chars since last newline)
|
||||
let mut column = 0;
|
||||
for c in content[..safe_pos].chars().rev() {
|
||||
if c == '\n' {
|
||||
break;
|
||||
}
|
||||
column += 1;
|
||||
}
|
||||
|
||||
(column + 1, self.current_cursor_line)
|
||||
} else {
|
||||
(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_page_movement(&mut self, ctx: &egui::Context, direction_down: bool) {
|
||||
let Some(active_tab) = self.get_active_tab() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let id_source = active_tab
|
||||
.file_path
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "untitled".to_string());
|
||||
let text_edit_id = egui::Id::new("main_text_editor").with(&id_source);
|
||||
|
||||
let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(cursor_range) = state.cursor.char_range() else {
|
||||
return;
|
||||
};
|
||||
let current_pos = cursor_range.primary.index;
|
||||
|
||||
let content = &active_tab.content;
|
||||
|
||||
let available_height = ctx.available_rect().height();
|
||||
let row_height = self.font_size * 1.5;
|
||||
let visible_rows = (available_height / row_height).floor() as usize;
|
||||
let rows_to_move = visible_rows.max(1);
|
||||
|
||||
let new_pos = if direction_down {
|
||||
move_cursor_down_lines(content, current_pos, rows_to_move)
|
||||
} else {
|
||||
move_cursor_up_lines(content, current_pos, rows_to_move)
|
||||
};
|
||||
|
||||
let new_cursor = egui::text::CCursor::new(new_pos);
|
||||
state
|
||||
.cursor
|
||||
.set_char_range(Some(egui::text::CCursorRange::one(new_cursor)));
|
||||
egui::TextEdit::store_state(ctx, text_edit_id, state);
|
||||
}
|
||||
}
|
||||
|
||||
fn move_cursor_down_lines(content: &str, current_pos: usize, lines: usize) -> usize {
|
||||
let safe_pos = current_pos.min(content.len());
|
||||
|
||||
let mut pos = safe_pos;
|
||||
let mut lines_moved = 0;
|
||||
|
||||
for (idx, ch) in content[safe_pos..].char_indices() {
|
||||
if ch == '\n' {
|
||||
lines_moved += 1;
|
||||
if lines_moved >= lines {
|
||||
pos = safe_pos + idx + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if lines_moved < lines && pos == safe_pos {
|
||||
pos = content.len();
|
||||
}
|
||||
|
||||
pos.min(content.len())
|
||||
}
|
||||
|
||||
fn move_cursor_up_lines(content: &str, current_pos: usize, lines: usize) -> usize {
|
||||
let safe_pos = current_pos.min(content.len());
|
||||
|
||||
let mut pos = safe_pos;
|
||||
let mut lines_moved = 0;
|
||||
|
||||
for ch in content[..safe_pos].chars().rev() {
|
||||
if pos > 0 {
|
||||
pos -= ch.len_utf8();
|
||||
}
|
||||
|
||||
if ch == '\n' {
|
||||
lines_moved += 1;
|
||||
if lines_moved >= lines {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pos
|
||||
}
|
||||
|
||||
@ -8,7 +8,8 @@ use std::path::PathBuf;
|
||||
mod app;
|
||||
mod io;
|
||||
mod ui;
|
||||
use app::{config::Config, TextEditor};
|
||||
mod util;
|
||||
use app::{TextEditor, config::Config};
|
||||
|
||||
fn main() -> eframe::Result {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
pub(crate) mod about_window;
|
||||
pub(crate) mod bottom_bar;
|
||||
pub(crate) mod central_panel;
|
||||
pub(crate) mod constants;
|
||||
pub(crate) mod find_window;
|
||||
|
||||
43
src/ui/bottom_bar.rs
Normal file
43
src/ui/bottom_bar.rs
Normal file
@ -0,0 +1,43 @@
|
||||
use crate::app::TextEditor;
|
||||
use crate::ui::central_panel::languages::get_language_from_extension;
|
||||
use eframe::egui::{self, Frame};
|
||||
|
||||
pub(crate) fn bottom_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
let line_count = app.get_text_processing_result().line_count;
|
||||
let char_count = app
|
||||
.get_active_tab()
|
||||
.map(|tab| tab.content.chars().count())
|
||||
.unwrap_or(0);
|
||||
let cursor_position = app.get_cursor_position();
|
||||
let cursor_column = cursor_position.0;
|
||||
let cursor_row = cursor_position.1;
|
||||
|
||||
let active_tab = app.get_active_tab();
|
||||
let file_path = active_tab.and_then(|tab| tab.file_path.as_deref());
|
||||
let language = get_language_from_extension(file_path);
|
||||
|
||||
if !app.hide_bottom_bar {
|
||||
let frame = Frame::NONE.fill(ctx.style().visuals.panel_fill);
|
||||
egui::TopBottomPanel::bottom("bottom_bar")
|
||||
.frame(frame)
|
||||
.show(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
|
||||
ui.add_space(8.0);
|
||||
ui.label(format!("Ln {}, Col {}", cursor_row, cursor_column));
|
||||
ui.separator();
|
||||
ui.label(format!("{} chars, {} lines", char_count, line_count));
|
||||
});
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.add_space(8.0);
|
||||
ui.label(format!("{}", language.to_uppercase()));
|
||||
ui.separator();
|
||||
ui.label("UTF-8");
|
||||
ui.separator();
|
||||
ui.label(format!("{}pt", app.font_size as u32));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,8 @@
|
||||
mod editor;
|
||||
mod find_highlight;
|
||||
mod languages;
|
||||
pub mod languages;
|
||||
mod line_numbers;
|
||||
mod markdown;
|
||||
|
||||
use crate::app::TextEditor;
|
||||
use crate::ui::constants::*;
|
||||
@ -9,7 +10,16 @@ use eframe::egui;
|
||||
use egui::UiKind;
|
||||
|
||||
use self::editor::editor_view_ui;
|
||||
use self::languages::get_language_from_extension;
|
||||
use self::line_numbers::{calculate_visual_line_mapping, render_line_numbers};
|
||||
use self::markdown::markdown_view_ui;
|
||||
|
||||
fn is_markdown_tab(app: &TextEditor) -> bool {
|
||||
app.get_active_tab()
|
||||
.and_then(|tab| tab.file_path.as_deref())
|
||||
.map(|path| get_language_from_extension(Some(path)) == "md")
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
let show_line_numbers = app.show_line_numbers;
|
||||
@ -17,6 +27,8 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
let line_side = app.line_side;
|
||||
let font_size = app.font_size;
|
||||
let font_id = app.get_font_id();
|
||||
let show_markdown = app.show_markdown;
|
||||
let is_markdown_file = is_markdown_tab(app);
|
||||
|
||||
let _output = egui::CentralPanel::default()
|
||||
.frame(egui::Frame::NONE)
|
||||
@ -26,6 +38,66 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
ui.painter().rect_filled(panel_rect, 0.0, bg_color);
|
||||
let editor_height = panel_rect.height();
|
||||
|
||||
// Handle markdown split view
|
||||
if show_markdown && is_markdown_file {
|
||||
let half_width = panel_rect.width() / 2.0;
|
||||
|
||||
ui.push_id("markdown_split_container", |ui| {
|
||||
ui.allocate_ui_with_layout(
|
||||
egui::vec2(panel_rect.width(), editor_height),
|
||||
egui::Layout::left_to_right(egui::Align::TOP),
|
||||
|ui| {
|
||||
// Left side: Editor
|
||||
ui.allocate_ui_with_layout(
|
||||
egui::vec2(half_width, editor_height),
|
||||
egui::Layout::top_down(egui::Align::LEFT),
|
||||
|ui| {
|
||||
egui::ScrollArea::vertical()
|
||||
.id_salt("editor_scroll_area")
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
editor_view_ui(ui, app);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Separator
|
||||
let separator_x = ui.cursor().left();
|
||||
let mut y_range = ui.available_rect_before_wrap().y_range();
|
||||
y_range.max += 2.0 * font_size;
|
||||
ui.painter()
|
||||
.vline(separator_x, y_range, ui.visuals().window_stroke);
|
||||
ui.add_space(SMALL);
|
||||
|
||||
// Right side: Markdown view
|
||||
ui.allocate_ui_with_layout(
|
||||
egui::vec2(half_width, editor_height),
|
||||
egui::Layout::top_down(egui::Align::LEFT),
|
||||
|ui| {
|
||||
egui::ScrollArea::vertical()
|
||||
.id_salt("markdown_scroll_area")
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
egui::Frame::new()
|
||||
.inner_margin(egui::Margin {
|
||||
left: 0,
|
||||
right: SCROLLBAR_WIDTH as i8,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
})
|
||||
.show(ui, |ui| {
|
||||
markdown_view_ui(ui, app);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if !show_line_numbers || app.get_active_tab().is_none() {
|
||||
let _scroll_response =
|
||||
egui::ScrollArea::vertical()
|
||||
@ -35,46 +107,19 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
let context_response =
|
||||
ui.allocate_response(full_rect.size(), egui::Sense::click());
|
||||
|
||||
ui.scope_builder(egui::UiBuilder::new().max_rect(full_rect), |ui| {
|
||||
editor_view_ui(ui, app);
|
||||
});
|
||||
let editor_response = ui
|
||||
.scope_builder(egui::UiBuilder::new().max_rect(full_rect), |ui| {
|
||||
editor_view_ui(ui, app)
|
||||
})
|
||||
.inner;
|
||||
|
||||
handle_empty(ui, app, &context_response);
|
||||
handle_empty(ui, app, &context_response, &editor_response);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let line_count = app.get_text_processing_result().line_count;
|
||||
let editor_dimensions = app.calculate_editor_dimensions(ui);
|
||||
let line_number_width = editor_dimensions.line_number_width;
|
||||
let editor_width = editor_dimensions.text_width - line_number_width;
|
||||
|
||||
let visual_line_mapping = if word_wrap {
|
||||
app.get_active_tab()
|
||||
.map(|active_tab| {
|
||||
let actual_editor_width = ui.available_width() - line_number_width;
|
||||
calculate_visual_line_mapping(
|
||||
ui,
|
||||
&active_tab.content,
|
||||
actual_editor_width,
|
||||
font_id,
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(Vec::new)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let line_numbers_widget = |ui: &mut egui::Ui| {
|
||||
render_line_numbers(
|
||||
ui,
|
||||
line_count,
|
||||
&visual_line_mapping,
|
||||
line_number_width,
|
||||
word_wrap,
|
||||
line_side,
|
||||
font_size,
|
||||
);
|
||||
};
|
||||
let line_number_width = app.calculate_editor_dimensions(ui).line_number_width;
|
||||
|
||||
let separator_widget = |ui: &mut egui::Ui| {
|
||||
let separator_x = ui.cursor().left();
|
||||
@ -88,13 +133,43 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
let available_width = ui.available_width();
|
||||
let actual_editor_width = (available_width - line_number_width).max(0.0);
|
||||
|
||||
let visual_line_mapping = if word_wrap {
|
||||
app.get_active_tab()
|
||||
.map(|active_tab| {
|
||||
calculate_visual_line_mapping(
|
||||
ui,
|
||||
&active_tab.content,
|
||||
actual_editor_width - (if line_side { 8.0 } else { 20.0 }),
|
||||
font_id,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let line_numbers_widget = |ui: &mut egui::Ui| {
|
||||
render_line_numbers(
|
||||
ui,
|
||||
line_count,
|
||||
&visual_line_mapping,
|
||||
line_number_width,
|
||||
word_wrap,
|
||||
line_side,
|
||||
font_size,
|
||||
);
|
||||
};
|
||||
|
||||
if line_side {
|
||||
ui.allocate_ui_with_layout(
|
||||
egui::vec2(editor_dimensions.text_width, editor_height),
|
||||
egui::vec2(available_width, editor_height),
|
||||
egui::Layout::left_to_right(egui::Align::TOP),
|
||||
|ui| {
|
||||
ui.allocate_ui_with_layout(
|
||||
egui::vec2(editor_width, editor_height),
|
||||
egui::vec2(actual_editor_width, editor_height),
|
||||
egui::Layout::left_to_right(egui::Align::TOP),
|
||||
|ui| {
|
||||
let full_rect: egui::Rect = ui.available_rect_before_wrap();
|
||||
@ -103,14 +178,14 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
egui::Sense::click(),
|
||||
);
|
||||
|
||||
ui.scope_builder(
|
||||
let editor_response = ui
|
||||
.scope_builder(
|
||||
egui::UiBuilder::new().max_rect(full_rect),
|
||||
|ui| {
|
||||
editor_view_ui(ui, app);
|
||||
},
|
||||
);
|
||||
|ui| editor_view_ui(ui, app),
|
||||
)
|
||||
.inner;
|
||||
|
||||
handle_empty(ui, app, &context_response);
|
||||
handle_empty(ui, app, &context_response, &editor_response);
|
||||
},
|
||||
);
|
||||
separator_widget(ui);
|
||||
@ -119,7 +194,7 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
);
|
||||
} else {
|
||||
ui.allocate_ui_with_layout(
|
||||
egui::vec2(editor_dimensions.text_width, editor_height),
|
||||
egui::vec2(available_width, editor_height),
|
||||
egui::Layout::left_to_right(egui::Align::TOP),
|
||||
|ui| {
|
||||
line_numbers_widget(ui);
|
||||
@ -129,14 +204,14 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
let context_response =
|
||||
ui.allocate_response(editor_area.size(), egui::Sense::click());
|
||||
|
||||
ui.scope_builder(
|
||||
let editor_response = ui
|
||||
.scope_builder(
|
||||
egui::UiBuilder::new().max_rect(editor_area),
|
||||
|ui| {
|
||||
editor_view_ui(ui, app);
|
||||
},
|
||||
);
|
||||
|ui| editor_view_ui(ui, app),
|
||||
)
|
||||
.inner;
|
||||
|
||||
handle_empty(ui, app, &context_response);
|
||||
handle_empty(ui, app, &context_response, &editor_response);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -144,27 +219,33 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_empty(_ui: &mut egui::Ui, app: &mut TextEditor, context_response: &egui::Response) {
|
||||
fn handle_empty(
|
||||
ui: &mut egui::Ui,
|
||||
app: &mut TextEditor,
|
||||
context_response: &egui::Response,
|
||||
editor_response: &egui::Response,
|
||||
) {
|
||||
if context_response.clicked() {
|
||||
let text_edit_id = egui::Id::new("main_text_editor");
|
||||
if let Some(mut state) = egui::TextEdit::load_state(_ui.ctx(), text_edit_id) {
|
||||
let text_edit_id = editor_response.id;
|
||||
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
||||
if let Some(active_tab) = app.get_active_tab() {
|
||||
let text_len = active_tab.content.len();
|
||||
let text_len = active_tab.content.chars().count();
|
||||
let cursor_pos = egui::text::CCursor::new(text_len);
|
||||
state
|
||||
.cursor
|
||||
.set_char_range(Some(egui::text::CCursorRange::one(cursor_pos)));
|
||||
egui::TextEdit::store_state(_ui.ctx(), text_edit_id, state);
|
||||
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
||||
|
||||
_ui.ctx().memory_mut(|mem| {
|
||||
ui.ctx().memory_mut(|mem| {
|
||||
mem.request_focus(text_edit_id);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context_response.context_menu(|ui| {
|
||||
let text_len = app.get_active_tab().unwrap().content.len();
|
||||
// Use the editor response for context menu so it captures right-clicks in the text area
|
||||
editor_response.clone().context_menu(|ui| {
|
||||
let text_len = app.get_active_tab().unwrap().content.chars().count();
|
||||
let reset_zoom_key = egui::Id::new("editor_reset_zoom");
|
||||
|
||||
if ui.button("Cut").clicked() {
|
||||
@ -195,7 +276,7 @@ fn handle_empty(_ui: &mut egui::Ui, app: &mut TextEditor, context_response: &egu
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.button("Select All").clicked() {
|
||||
let text_edit_id = egui::Id::new("main_text_editor");
|
||||
let text_edit_id = editor_response.id;
|
||||
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),
|
||||
|
||||
@ -5,16 +5,16 @@ use egui_extras::syntax_highlighting::{self};
|
||||
use super::find_highlight;
|
||||
|
||||
pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::Response {
|
||||
let _current_match_position = app.get_current_match_position();
|
||||
let show_find = app.show_find;
|
||||
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;
|
||||
let font_id = app.get_font_id();
|
||||
let syntax_highlighting_enabled = app.syntax_highlighting;
|
||||
let previous_cursor_position = app.previous_cursor_position;
|
||||
|
||||
let bg_color = ui.visuals().extreme_bg_color;
|
||||
let editor_rect = ui.available_rect_before_wrap();
|
||||
@ -55,18 +55,21 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
||||
return ui.label("No file open, how did you get here?");
|
||||
};
|
||||
|
||||
let draw_highlights = |ui: &mut egui::Ui| {
|
||||
if let Some((content, matches, current_match_index)) = &find_data {
|
||||
let temp_galley = ui.fonts(|fonts| {
|
||||
let temp_galley = ui.fonts_mut(|fonts| {
|
||||
fonts.layout(
|
||||
content.to_owned(),
|
||||
font_id.to_owned(),
|
||||
ui.visuals().text_color(),
|
||||
desired_width,
|
||||
desired_width - 8.0,
|
||||
)
|
||||
});
|
||||
|
||||
let text_area_left = editor_rect.left() + 4.0; // Text Editor default margins
|
||||
let text_area_top = editor_rect.top() + 2.0;
|
||||
// Use the current cursor position which handles scroll offsets correctly
|
||||
let cursor_pos = ui.cursor().min;
|
||||
let text_area_left = cursor_pos.x + 4.0; // Text Editor default margins
|
||||
let text_area_top = cursor_pos.y + 2.0;
|
||||
|
||||
find_highlight::draw_find_highlights(
|
||||
ui,
|
||||
@ -79,6 +82,7 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
||||
font_size,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let language = super::languages::get_language_from_extension(active_tab.file_path.as_deref());
|
||||
let mut layouter = |ui: &egui::Ui, string: &dyn egui::TextBuffer, wrap_width: f32| {
|
||||
@ -102,9 +106,21 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
||||
}
|
||||
|
||||
layout_job.wrap.max_width = wrap_width;
|
||||
ui.fonts(|f| f.layout_job(layout_job))
|
||||
ui.fonts_mut(|f| f.layout_job(layout_job))
|
||||
};
|
||||
|
||||
let id_source = active_tab
|
||||
.file_path
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "untitled".to_string());
|
||||
let text_edit_id = egui::Id::new("main_text_editor").with(&id_source);
|
||||
|
||||
let allow_interaction = ui.is_enabled()
|
||||
&& !ui.input(|i| {
|
||||
i.pointer.button_down(egui::PointerButton::Secondary)
|
||||
|| i.pointer.button_down(egui::PointerButton::Middle)
|
||||
});
|
||||
let text_edit = egui::TextEdit::multiline(&mut active_tab.content)
|
||||
.frame(false)
|
||||
.code_editor()
|
||||
@ -113,9 +129,43 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
||||
.lock_focus(!show_find)
|
||||
.cursor_at_end(false)
|
||||
.layouter(&mut layouter)
|
||||
.id(egui::Id::new("main_text_editor"));
|
||||
.interactive(allow_interaction)
|
||||
.id(text_edit_id);
|
||||
|
||||
let ensure_cursor_visible = |ui: &mut egui::Ui,
|
||||
output: &egui::text_edit::TextEditOutput,
|
||||
font_id: &egui::FontId| {
|
||||
let current_cursor_pos = output
|
||||
.state
|
||||
.cursor
|
||||
.char_range()
|
||||
.map(|range| range.primary.index);
|
||||
|
||||
if let Some(cursor_pos) = current_cursor_pos {
|
||||
let cursor_moved = Some(cursor_pos) != previous_cursor_position;
|
||||
let text_changed = output.response.changed();
|
||||
|
||||
if cursor_moved || text_changed {
|
||||
let cursor_rect = output
|
||||
.galley
|
||||
.pos_from_cursor(egui::text::CCursor::new(cursor_pos));
|
||||
|
||||
let global_cursor_rect = cursor_rect.translate(output.response.rect.min.to_vec2());
|
||||
|
||||
let line_height = ui.fonts_mut(|fonts| fonts.row_height(font_id));
|
||||
let margin = egui::vec2(40.0, line_height * 2.0);
|
||||
let target_rect = global_cursor_rect.expand2(margin);
|
||||
|
||||
let visible_area = ui.clip_rect();
|
||||
if !visible_area.contains_rect(target_rect) {
|
||||
ui.scroll_to_rect(target_rect, Some(egui::Align::Center));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let output = if word_wrap {
|
||||
draw_highlights(ui);
|
||||
text_edit.show(ui)
|
||||
} else {
|
||||
egui::ScrollArea::horizontal()
|
||||
@ -124,13 +174,20 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
||||
ui.allocate_ui_with_layout(
|
||||
egui::Vec2::new(estimated_width, ui.available_height()),
|
||||
egui::Layout::left_to_right(egui::Align::TOP),
|
||||
|ui| text_edit.show(ui),
|
||||
|ui| {
|
||||
draw_highlights(ui);
|
||||
let output = text_edit.show(ui);
|
||||
ensure_cursor_visible(ui, &output, &font_id);
|
||||
output
|
||||
},
|
||||
)
|
||||
})
|
||||
.inner
|
||||
.inner
|
||||
};
|
||||
|
||||
ensure_cursor_visible(ui, &output, &font_id);
|
||||
|
||||
let content_changed = output.response.changed();
|
||||
let content_for_processing = if content_changed {
|
||||
active_tab.update_modified_state();
|
||||
@ -139,11 +196,9 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
||||
None
|
||||
};
|
||||
|
||||
if content_changed {
|
||||
if let Err(e) = app.save_state_cache() {
|
||||
if content_changed && let Err(e) = app.save_state_cache() {
|
||||
eprintln!("Failed to save state cache: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
if content_changed && app.show_find && !app.find_query.is_empty() {
|
||||
app.update_find_matches();
|
||||
@ -189,40 +244,19 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
||||
}
|
||||
|
||||
if let Some(cursor_pos) = current_cursor_pos {
|
||||
let cursor_moved = Some(cursor_pos) != app.previous_cursor_position;
|
||||
let text_changed = output.response.changed();
|
||||
app.previous_cursor_position = Some(cursor_pos);
|
||||
app.current_cursor_index = cursor_pos;
|
||||
|
||||
if cursor_moved || text_changed {
|
||||
// Calculate line and column
|
||||
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 safe_pos = cursor_pos.min(content.len());
|
||||
|
||||
let font_id = ui
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&egui::TextStyle::Monospace)
|
||||
.unwrap_or(&egui::FontId::monospace(font_size))
|
||||
.to_owned();
|
||||
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));
|
||||
// Count newlines before cursor for line number
|
||||
let line_number = content[..safe_pos].chars().filter(|&c| c == '\n').count() + 1;
|
||||
app.current_cursor_line = line_number;
|
||||
}
|
||||
}
|
||||
}
|
||||
app.previous_cursor_position = Some(cursor_pos);
|
||||
}
|
||||
|
||||
if !output.response.has_focus()
|
||||
&& !show_preferences
|
||||
|
||||
@ -1,15 +1,6 @@
|
||||
use crate::util::safe_slice_to_pos;
|
||||
use eframe::egui;
|
||||
|
||||
/// Safely get a string slice up to a byte position, ensuring UTF-8 boundaries
|
||||
fn safe_slice_to_pos(content: &str, pos: usize) -> &str {
|
||||
let pos = pos.min(content.len());
|
||||
let mut boundary_pos = pos;
|
||||
while boundary_pos > 0 && !content.is_char_boundary(boundary_pos) {
|
||||
boundary_pos -= 1;
|
||||
}
|
||||
&content[..boundary_pos]
|
||||
}
|
||||
|
||||
pub(super) fn draw_find_highlights(
|
||||
ui: &mut egui::Ui,
|
||||
content: &str,
|
||||
@ -18,26 +9,18 @@ pub(super) fn draw_find_highlights(
|
||||
galley: &std::sync::Arc<egui::Galley>,
|
||||
text_area_left: f32,
|
||||
text_area_top: f32,
|
||||
font_size: f32,
|
||||
_font_size: f32,
|
||||
) {
|
||||
let font_id = ui
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&egui::TextStyle::Monospace)
|
||||
.unwrap_or(&egui::FontId::monospace(font_size))
|
||||
.to_owned();
|
||||
|
||||
for (match_index, &(start_pos, end_pos)) in matches.iter().enumerate() {
|
||||
for (match_index, &(start_byte, end_byte)) in matches.iter().enumerate() {
|
||||
let is_current_match = current_match_index == Some(match_index);
|
||||
draw_single_highlight(
|
||||
ui,
|
||||
content,
|
||||
start_pos,
|
||||
end_pos,
|
||||
start_byte,
|
||||
end_byte,
|
||||
galley,
|
||||
text_area_left,
|
||||
text_area_top,
|
||||
galley,
|
||||
&font_id,
|
||||
is_current_match,
|
||||
);
|
||||
}
|
||||
@ -46,70 +29,15 @@ pub(super) fn draw_find_highlights(
|
||||
fn draw_single_highlight(
|
||||
ui: &mut egui::Ui,
|
||||
content: &str,
|
||||
start_pos: usize,
|
||||
end_pos: usize,
|
||||
start_byte: usize,
|
||||
end_byte: usize,
|
||||
galley: &std::sync::Arc<egui::Galley>,
|
||||
text_area_left: f32,
|
||||
text_area_top: f32,
|
||||
galley: &std::sync::Arc<egui::Galley>,
|
||||
font_id: &egui::FontId,
|
||||
is_current_match: bool,
|
||||
) {
|
||||
let text_up_to_start = safe_slice_to_pos(content, start_pos);
|
||||
let start_line = text_up_to_start.chars().filter(|&c| c == '\n').count();
|
||||
|
||||
if start_line >= galley.rows.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
let line_start_byte_pos = text_up_to_start.rfind('\n').map(|pos| pos + 1).unwrap_or(0);
|
||||
let line_start_char_pos = safe_slice_to_pos(content, line_start_byte_pos)
|
||||
.chars()
|
||||
.count();
|
||||
let start_char_pos = safe_slice_to_pos(content, start_pos).chars().count();
|
||||
let start_col = start_char_pos - line_start_char_pos;
|
||||
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
if start_line >= lines.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
let line_text = lines[start_line];
|
||||
let text_before_match: String = line_text.chars().take(start_col).collect();
|
||||
|
||||
let text_before_width = ui.fonts(|fonts| {
|
||||
fonts
|
||||
.layout(
|
||||
text_before_match,
|
||||
font_id.to_owned(),
|
||||
egui::Color32::WHITE,
|
||||
f32::INFINITY,
|
||||
)
|
||||
.size()
|
||||
.x
|
||||
});
|
||||
|
||||
let galley_row = &galley.rows[start_line];
|
||||
let start_y = text_area_top + galley_row.min_y();
|
||||
let line_height = galley_row.height();
|
||||
let start_x = text_area_left + text_before_width;
|
||||
|
||||
let match_text = &content[start_pos..end_pos.min(content.len())];
|
||||
let match_width = ui.fonts(|fonts| {
|
||||
fonts
|
||||
.layout(
|
||||
match_text.to_string(),
|
||||
font_id.to_owned(),
|
||||
ui.visuals().text_color(),
|
||||
f32::INFINITY,
|
||||
)
|
||||
.size()
|
||||
.x
|
||||
});
|
||||
|
||||
let highlight_rect = egui::Rect::from_min_size(
|
||||
egui::pos2(start_x, start_y),
|
||||
egui::vec2(match_width, line_height),
|
||||
);
|
||||
let start_char = safe_slice_to_pos(content, start_byte).chars().count();
|
||||
let end_char = safe_slice_to_pos(content, end_byte).chars().count();
|
||||
|
||||
let highlight_color = if is_current_match {
|
||||
ui.visuals().selection.bg_fill
|
||||
@ -118,5 +46,36 @@ fn draw_single_highlight(
|
||||
};
|
||||
|
||||
let painter = ui.painter();
|
||||
painter.rect_filled(highlight_rect, 0.0, highlight_color);
|
||||
|
||||
let mut current_char_idx = 0;
|
||||
|
||||
for row in &galley.rows {
|
||||
let row_start_char = current_char_idx;
|
||||
let row_end_char = row_start_char + row.char_count_excluding_newline();
|
||||
|
||||
current_char_idx += row.char_count_including_newline();
|
||||
|
||||
if row_end_char <= start_char || row_start_char >= end_char {
|
||||
continue;
|
||||
}
|
||||
|
||||
let highlight_start_char_in_row = start_char.max(row_start_char) - row_start_char;
|
||||
let highlight_end_char_in_row = end_char.min(row_end_char) - row_start_char;
|
||||
|
||||
let start_x = row.x_offset(highlight_start_char_in_row);
|
||||
let end_x = row.x_offset(highlight_end_char_in_row);
|
||||
|
||||
let rect = egui::Rect::from_min_max(
|
||||
egui::pos2(
|
||||
text_area_left + row.rect().min.x + start_x,
|
||||
text_area_top + row.rect().min.y,
|
||||
),
|
||||
egui::pos2(
|
||||
text_area_left + row.rect().min.x + end_x,
|
||||
text_area_top + row.rect().max.y,
|
||||
),
|
||||
);
|
||||
|
||||
painter.rect_filled(rect, 0.0, highlight_color);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,8 +2,10 @@ use eframe::egui;
|
||||
|
||||
fn format_line_number(line_number: usize, line_side: bool, line_count_width: usize) -> String {
|
||||
if line_side {
|
||||
// Right side: left-align with trailing space for scrollbar clearance
|
||||
format!("{:<width$} ", line_number, width = line_count_width)
|
||||
} else {
|
||||
// Left side: right-align, no trailing space (separator provides gap)
|
||||
format!("{:>width$}", line_number, width = line_count_width)
|
||||
}
|
||||
}
|
||||
@ -22,12 +24,12 @@ pub(super) fn calculate_visual_line_mapping(
|
||||
continue;
|
||||
}
|
||||
|
||||
let galley = ui.fonts(|fonts| {
|
||||
let galley = ui.fonts_mut(|fonts| {
|
||||
fonts.layout(
|
||||
line.to_string(),
|
||||
font_id.to_owned(),
|
||||
egui::Color32::WHITE,
|
||||
available_width - font_id.size,
|
||||
available_width,
|
||||
)
|
||||
});
|
||||
|
||||
@ -60,7 +62,7 @@ pub(super) fn render_line_numbers(
|
||||
ui.disable();
|
||||
ui.set_width(line_number_width);
|
||||
ui.spacing_mut().item_spacing.y = 0.0;
|
||||
ui.add_space(2.0); // Text Editor default top margin
|
||||
ui.add_space(1.0); // Text Editor default top margin
|
||||
let text_color = ui.visuals().weak_text_color();
|
||||
let bg_color = ui.visuals().extreme_bg_color;
|
||||
|
||||
|
||||
14
src/ui/central_panel/markdown.rs
Normal file
14
src/ui/central_panel/markdown.rs
Normal file
@ -0,0 +1,14 @@
|
||||
use crate::app::TextEditor;
|
||||
use eframe::egui;
|
||||
use egui_commonmark::{CommonMarkViewer, CommonMarkCache};
|
||||
|
||||
pub(super) fn markdown_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) {
|
||||
let Some(active_tab) = app.get_active_tab() else {
|
||||
ui.label("No file open");
|
||||
return;
|
||||
};
|
||||
|
||||
// Properly render Markdown content using CommonMarkViewer
|
||||
let mut cache = CommonMarkCache::default();
|
||||
CommonMarkViewer::new().show(ui, &mut cache, &active_tab.content);
|
||||
}
|
||||
@ -24,3 +24,5 @@ pub const DEFAULT_FONT_SIZE_STR: &str = "14";
|
||||
pub const PREVIEW_AREA_MAX_HEIGHT: f32 = 150.0;
|
||||
|
||||
pub const INNER_MARGIN: i8 = 8;
|
||||
|
||||
pub const SCROLLBAR_WIDTH: f32 = 25.0;
|
||||
|
||||
@ -59,7 +59,7 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
let response = ui.add(
|
||||
egui::TextEdit::singleline(&mut app.find_query)
|
||||
.desired_width(250.0)
|
||||
.hint_text("Enter search text..."),
|
||||
.hint_text("Search..."),
|
||||
);
|
||||
|
||||
if response.changed() {
|
||||
@ -84,7 +84,7 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
let _replace_response = ui.add(
|
||||
egui::TextEdit::singleline(&mut app.replace_query)
|
||||
.desired_width(250.0)
|
||||
.hint_text("Enter replacement text..."),
|
||||
.hint_text("Replace..."),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -103,12 +103,19 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.button("Select All").clicked() {
|
||||
let text_edit_id = egui::Id::new("main_text_editor");
|
||||
if let Some(active_tab) = app.get_active_tab() {
|
||||
let id_source = active_tab
|
||||
.file_path
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "untitled".to_string());
|
||||
let text_edit_id =
|
||||
egui::Id::new("main_text_editor").with(&id_source);
|
||||
if let Some(mut state) =
|
||||
egui::TextEdit::load_state(ui.ctx(), text_edit_id)
|
||||
{
|
||||
if let Some(active_tab) = app.get_active_tab() {
|
||||
let text_len = active_tab.content.len();
|
||||
let text_len = active_tab.content.chars().count();
|
||||
let select_all_range = egui::text::CCursorRange::two(
|
||||
egui::text::CCursor::new(0),
|
||||
egui::text::CCursor::new(text_len),
|
||||
@ -117,11 +124,40 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("Undo").clicked() {
|
||||
let text_edit_id = egui::Id::new("main_text_editor");
|
||||
// Check if undo is available
|
||||
let can_undo = if let Some(active_tab) = app.get_active_tab() {
|
||||
let id_source = active_tab
|
||||
.file_path
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "untitled".to_string());
|
||||
let text_edit_id = egui::Id::new("main_text_editor").with(&id_source);
|
||||
if let Some(state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
||||
let current_state = (
|
||||
state.cursor.char_range().unwrap_or_default(),
|
||||
active_tab.content.to_string(),
|
||||
);
|
||||
state.undoer().undo(¤t_state).is_some()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if ui.add_enabled(can_undo, egui::Button::new("Undo")).clicked() {
|
||||
if let Some(active_tab) = app.get_active_tab_mut() {
|
||||
let id_source = active_tab
|
||||
.file_path
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "untitled".to_string());
|
||||
let text_edit_id =
|
||||
egui::Id::new("main_text_editor").with(&id_source);
|
||||
if let Some(mut state) =
|
||||
egui::TextEdit::load_state(ui.ctx(), text_edit_id)
|
||||
{
|
||||
@ -137,7 +173,11 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
active_tab.content = content.to_string();
|
||||
state.cursor.set_char_range(Some(*cursor_range));
|
||||
state.set_undoer(undoer);
|
||||
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
||||
egui::TextEdit::store_state(
|
||||
ui.ctx(),
|
||||
text_edit_id,
|
||||
state,
|
||||
);
|
||||
active_tab.update_modified_state();
|
||||
if app.show_find && !app.find_query.is_empty() {
|
||||
app.update_find_matches();
|
||||
@ -145,10 +185,39 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.button("Redo").clicked() {
|
||||
let text_edit_id = egui::Id::new("main_text_editor");
|
||||
// Check if redo is available
|
||||
let can_redo = if let Some(active_tab) = app.get_active_tab() {
|
||||
let id_source = active_tab
|
||||
.file_path
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "untitled".to_string());
|
||||
let text_edit_id = egui::Id::new("main_text_editor").with(&id_source);
|
||||
if let Some(state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
||||
let current_state = (
|
||||
state.cursor.char_range().unwrap_or_default(),
|
||||
active_tab.content.to_string(),
|
||||
);
|
||||
state.undoer().redo(¤t_state).is_some()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if ui.add_enabled(can_redo, egui::Button::new("Redo")).clicked() {
|
||||
if let Some(active_tab) = app.get_active_tab_mut() {
|
||||
let id_source = active_tab
|
||||
.file_path
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "untitled".to_string());
|
||||
let text_edit_id =
|
||||
egui::Id::new("main_text_editor").with(&id_source);
|
||||
if let Some(mut state) =
|
||||
egui::TextEdit::load_state(ui.ctx(), text_edit_id)
|
||||
{
|
||||
@ -164,7 +233,11 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
active_tab.content = content.to_string();
|
||||
state.cursor.set_char_range(Some(*cursor_range));
|
||||
state.set_undoer(undoer);
|
||||
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
||||
egui::TextEdit::store_state(
|
||||
ui.ctx(),
|
||||
text_edit_id,
|
||||
state,
|
||||
);
|
||||
active_tab.update_modified_state();
|
||||
if app.show_find && !app.find_query.is_empty() {
|
||||
app.update_find_matches();
|
||||
@ -172,6 +245,7 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
});
|
||||
@ -192,6 +266,13 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
app.save_config();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui
|
||||
.checkbox(&mut app.show_markdown, "Preview Markdown")
|
||||
.clicked()
|
||||
{
|
||||
app.save_config();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.checkbox(&mut app.word_wrap, "Word Wrap").clicked() {
|
||||
app.save_config();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
@ -200,6 +281,10 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
app.save_config();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.checkbox(&mut app.hide_bottom_bar, "Hide Bottom Bar").clicked() {
|
||||
app.save_config();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui
|
||||
.checkbox(&mut app.auto_hide_toolbar, "Auto Hide Toolbar")
|
||||
.clicked()
|
||||
@ -289,10 +374,10 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
empty_tab.get_display_title()
|
||||
};
|
||||
|
||||
let window_width = ctx.screen_rect().width();
|
||||
let window_width = ctx.viewport_rect().width();
|
||||
let font_id = ui.style().text_styles[&egui::TextStyle::Body].to_owned();
|
||||
|
||||
let text_galley = ui.fonts(|fonts| {
|
||||
let text_galley = ui.fonts_mut(|fonts| {
|
||||
fonts.layout_job(egui::text::LayoutJob::simple_singleline(
|
||||
tab_title,
|
||||
font_id,
|
||||
|
||||
@ -4,7 +4,7 @@ use eframe::egui;
|
||||
|
||||
pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
let visuals = &ctx.style().visuals;
|
||||
let screen_rect = ctx.screen_rect();
|
||||
let screen_rect = ctx.viewport_rect();
|
||||
let window_width =
|
||||
(screen_rect.width() * WINDOW_WIDTH_RATIO).clamp(WINDOW_MIN_WIDTH, WINDOW_MAX_WIDTH);
|
||||
let window_height =
|
||||
|
||||
@ -40,6 +40,8 @@ fn render_shortcuts_content(ui: &mut egui::Ui) {
|
||||
);
|
||||
ui.label(egui::RichText::new("Ctrl + K: Toggle Word Wrap").size(UI_TEXT_SIZE));
|
||||
ui.label(egui::RichText::new("Ctrl + H: Toggle Auto Hide Toolbar").size(UI_TEXT_SIZE));
|
||||
ui.label(egui::RichText::new("Ctrl + B: Toggle Bottom Bar").size(UI_TEXT_SIZE));
|
||||
ui.label(egui::RichText::new("Ctrl + M: Toggle Markdown Preview").size(UI_TEXT_SIZE));
|
||||
ui.label(egui::RichText::new("Ctrl + P: Preferences").size(UI_TEXT_SIZE));
|
||||
ui.label(egui::RichText::new("Ctrl + =/-: Increase/Decrease Font Size").size(UI_TEXT_SIZE));
|
||||
ui.label(egui::RichText::new("Ctrl + Shift + =/-: Zoom In/Out").size(UI_TEXT_SIZE));
|
||||
@ -58,7 +60,7 @@ 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();
|
||||
let screen_rect = ctx.viewport_rect();
|
||||
|
||||
let window_width =
|
||||
(screen_rect.width() * WINDOW_WIDTH_RATIO).clamp(WINDOW_MIN_WIDTH, WINDOW_MAX_WIDTH);
|
||||
|
||||
8
src/util.rs
Normal file
8
src/util.rs
Normal file
@ -0,0 +1,8 @@
|
||||
pub(crate) fn safe_slice_to_pos(content: &str, pos: usize) -> &str {
|
||||
let pos = pos.min(content.len());
|
||||
let mut boundary_pos = pos;
|
||||
while boundary_pos > 0 && !content.is_char_boundary(boundary_pos) {
|
||||
boundary_pos -= 1;
|
||||
}
|
||||
&content[..boundary_pos]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user