markdown, better wrapping
This commit is contained in:
parent
a3158129d1
commit
d2fb8bf8ed
@ -1,12 +1,12 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ced"
|
name = "ced"
|
||||||
version = "0.1.3"
|
version = "0.2.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
eframe = "0.32"
|
eframe = "0.33.3"
|
||||||
egui = "0.32"
|
egui = "0.33.3"
|
||||||
egui_extras = { version = "0.32", features = ["syntect"] }
|
egui_extras = { version = "0.33.3", features = ["syntect"] }
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.141"
|
serde_json = "1.0.141"
|
||||||
rfd = "0.15.4"
|
rfd = "0.15.4"
|
||||||
@ -17,3 +17,4 @@ syntect = "5.2.0"
|
|||||||
plist = "1.7.4"
|
plist = "1.7.4"
|
||||||
diffy = "0.4.2"
|
diffy = "0.4.2"
|
||||||
uuid = { version = "1.0", features = ["v4"] }
|
uuid = { version = "1.0", features = ["v4"] }
|
||||||
|
egui_commonmark = { version = "0.22" }
|
||||||
@ -14,8 +14,10 @@ enum ShortcutAction {
|
|||||||
ToggleLineSide,
|
ToggleLineSide,
|
||||||
ToggleWordWrap,
|
ToggleWordWrap,
|
||||||
ToggleAutoHideToolbar,
|
ToggleAutoHideToolbar,
|
||||||
|
ToggleBottomBar,
|
||||||
ToggleFind,
|
ToggleFind,
|
||||||
ToggleReplace,
|
ToggleReplace,
|
||||||
|
ToggleMarkdown,
|
||||||
FocusFind,
|
FocusFind,
|
||||||
NextTab,
|
NextTab,
|
||||||
PrevTab,
|
PrevTab,
|
||||||
@ -92,6 +94,11 @@ fn get_shortcuts() -> Vec<ShortcutDefinition> {
|
|||||||
egui::Key::H,
|
egui::Key::H,
|
||||||
ShortcutAction::ToggleAutoHideToolbar,
|
ShortcutAction::ToggleAutoHideToolbar,
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
egui::Modifiers::CTRL,
|
||||||
|
egui::Key::B,
|
||||||
|
ShortcutAction::ToggleBottomBar,
|
||||||
|
),
|
||||||
(
|
(
|
||||||
egui::Modifiers::CTRL | egui::Modifiers::SHIFT,
|
egui::Modifiers::CTRL | egui::Modifiers::SHIFT,
|
||||||
egui::Key::Tab,
|
egui::Key::Tab,
|
||||||
@ -152,6 +159,11 @@ fn get_shortcuts() -> Vec<ShortcutDefinition> {
|
|||||||
egui::Key::Escape,
|
egui::Key::Escape,
|
||||||
ShortcutAction::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();
|
editor.save_config();
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
ShortcutAction::ToggleBottomBar => {
|
||||||
|
editor.hide_bottom_bar = !editor.hide_bottom_bar;
|
||||||
|
editor.save_config();
|
||||||
|
false
|
||||||
|
}
|
||||||
ShortcutAction::NextTab => {
|
ShortcutAction::NextTab => {
|
||||||
let next_tab_index = editor.active_tab_index + 1;
|
let next_tab_index = editor.active_tab_index + 1;
|
||||||
if next_tab_index < editor.tabs.len() {
|
if next_tab_index < editor.tabs.len() {
|
||||||
@ -228,8 +245,9 @@ fn execute_action(action: ShortcutAction, editor: &mut TextEditor) -> bool {
|
|||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
ShortcutAction::PageUp => false,
|
ShortcutAction::PageUp | ShortcutAction::PageDown => {
|
||||||
ShortcutAction::PageDown => false,
|
false
|
||||||
|
}
|
||||||
ShortcutAction::ZoomIn => {
|
ShortcutAction::ZoomIn => {
|
||||||
editor.font_size += 1.0;
|
editor.font_size += 1.0;
|
||||||
true
|
true
|
||||||
@ -293,12 +311,18 @@ fn execute_action(action: ShortcutAction, editor: &mut TextEditor) -> bool {
|
|||||||
editor.show_preferences = !editor.show_preferences;
|
editor.show_preferences = !editor.show_preferences;
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
ShortcutAction::ToggleMarkdown => {
|
||||||
|
editor.show_markdown = !editor.show_markdown;
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle(editor: &mut TextEditor, ctx: &egui::Context) {
|
pub fn handle(editor: &mut TextEditor, ctx: &egui::Context) {
|
||||||
let mut font_zoom_occurred = false;
|
let mut font_zoom_occurred = false;
|
||||||
let mut global_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| {
|
ctx.input_mut(|i| {
|
||||||
for (modifiers, key, action) in get_shortcuts() {
|
for (modifiers, key, action) in get_shortcuts() {
|
||||||
@ -313,6 +337,12 @@ pub fn handle(editor: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
execute_action(action, editor);
|
execute_action(action, editor);
|
||||||
global_zoom_occurred = true;
|
global_zoom_occurred = true;
|
||||||
}
|
}
|
||||||
|
ShortcutAction::PageUp => {
|
||||||
|
page_up_pressed = true;
|
||||||
|
}
|
||||||
|
ShortcutAction::PageDown => {
|
||||||
|
page_down_pressed = true;
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
execute_action(action, editor);
|
execute_action(action, editor);
|
||||||
}
|
}
|
||||||
@ -330,6 +360,14 @@ pub fn handle(editor: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
ctx.set_zoom_factor(editor.zoom_factor);
|
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 {
|
if editor.should_select_current_match {
|
||||||
editor.select_current_match(ctx);
|
editor.select_current_match(ctx);
|
||||||
editor.should_select_current_match = false;
|
editor.should_select_current_match = false;
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
use super::editor::TextEditor;
|
use super::editor::TextEditor;
|
||||||
use crate::app::shortcuts;
|
use crate::app::shortcuts;
|
||||||
use crate::ui::about_window::about_window;
|
use crate::ui::about_window::about_window;
|
||||||
|
use crate::ui::bottom_bar::bottom_bar;
|
||||||
use crate::ui::central_panel::central_panel;
|
use crate::ui::central_panel::central_panel;
|
||||||
use crate::ui::find_window::find_window;
|
use crate::ui::find_window::find_window;
|
||||||
use crate::ui::menu_bar::menu_bar;
|
use crate::ui::menu_bar::menu_bar;
|
||||||
@ -28,6 +29,10 @@ impl eframe::App for TextEditor {
|
|||||||
tab_bar(self, ctx);
|
tab_bar(self, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !self.hide_bottom_bar {
|
||||||
|
bottom_bar(self, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
central_panel(self, ctx);
|
central_panel(self, ctx);
|
||||||
|
|
||||||
if self.show_about {
|
if self.show_about {
|
||||||
|
|||||||
@ -14,6 +14,7 @@ impl Default for TextEditor {
|
|||||||
show_shortcuts: false,
|
show_shortcuts: false,
|
||||||
show_find: false,
|
show_find: false,
|
||||||
show_preferences: false,
|
show_preferences: false,
|
||||||
|
show_markdown: false,
|
||||||
pending_unsaved_action: None,
|
pending_unsaved_action: None,
|
||||||
force_quit_confirmed: false,
|
force_quit_confirmed: false,
|
||||||
clean_quit_requested: false,
|
clean_quit_requested: false,
|
||||||
@ -21,6 +22,7 @@ impl Default for TextEditor {
|
|||||||
word_wrap: true,
|
word_wrap: true,
|
||||||
auto_hide_toolbar: false,
|
auto_hide_toolbar: false,
|
||||||
hide_tab_bar: true,
|
hide_tab_bar: true,
|
||||||
|
hide_bottom_bar: false,
|
||||||
syntax_highlighting: false,
|
syntax_highlighting: false,
|
||||||
theme: Theme::default(),
|
theme: Theme::default(),
|
||||||
line_side: false,
|
line_side: false,
|
||||||
@ -45,6 +47,7 @@ impl Default for TextEditor {
|
|||||||
previous_content: String::new(),
|
previous_content: String::new(),
|
||||||
previous_cursor_char_index: None,
|
previous_cursor_char_index: None,
|
||||||
current_cursor_line: 0,
|
current_cursor_line: 0,
|
||||||
|
current_cursor_index: 0,
|
||||||
previous_cursor_line: 0,
|
previous_cursor_line: 0,
|
||||||
font_settings_changed: false,
|
font_settings_changed: false,
|
||||||
text_needs_processing: false,
|
text_needs_processing: false,
|
||||||
|
|||||||
@ -39,6 +39,7 @@ pub struct TextEditor {
|
|||||||
pub(crate) show_shortcuts: bool,
|
pub(crate) show_shortcuts: bool,
|
||||||
pub(crate) show_find: bool,
|
pub(crate) show_find: bool,
|
||||||
pub(crate) show_preferences: bool,
|
pub(crate) show_preferences: bool,
|
||||||
|
pub(crate) show_markdown: bool,
|
||||||
pub(crate) pending_unsaved_action: Option<UnsavedAction>,
|
pub(crate) pending_unsaved_action: Option<UnsavedAction>,
|
||||||
pub(crate) force_quit_confirmed: bool,
|
pub(crate) force_quit_confirmed: bool,
|
||||||
pub(crate) clean_quit_requested: bool,
|
pub(crate) clean_quit_requested: bool,
|
||||||
@ -46,6 +47,7 @@ pub struct TextEditor {
|
|||||||
pub(crate) word_wrap: bool,
|
pub(crate) word_wrap: bool,
|
||||||
pub(crate) auto_hide_toolbar: bool,
|
pub(crate) auto_hide_toolbar: bool,
|
||||||
pub(crate) hide_tab_bar: bool,
|
pub(crate) hide_tab_bar: bool,
|
||||||
|
pub(crate) hide_bottom_bar: bool,
|
||||||
pub(crate) syntax_highlighting: bool,
|
pub(crate) syntax_highlighting: bool,
|
||||||
pub(crate) theme: Theme,
|
pub(crate) theme: Theme,
|
||||||
pub(crate) line_side: bool,
|
pub(crate) line_side: bool,
|
||||||
@ -69,6 +71,7 @@ pub struct TextEditor {
|
|||||||
pub(crate) previous_content: String,
|
pub(crate) previous_content: String,
|
||||||
pub(crate) previous_cursor_char_index: Option<usize>,
|
pub(crate) previous_cursor_char_index: Option<usize>,
|
||||||
pub(crate) current_cursor_line: usize,
|
pub(crate) current_cursor_line: usize,
|
||||||
|
pub(crate) current_cursor_index: usize,
|
||||||
pub(crate) previous_cursor_line: usize,
|
pub(crate) previous_cursor_line: usize,
|
||||||
pub(crate) font_settings_changed: bool,
|
pub(crate) font_settings_changed: bool,
|
||||||
pub(crate) text_needs_processing: bool,
|
pub(crate) text_needs_processing: bool,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
use super::editor::TextEditor;
|
use super::editor::TextEditor;
|
||||||
|
use crate::util::safe_slice_to_pos;
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
|
|
||||||
impl TextEditor {
|
impl TextEditor {
|
||||||
@ -114,10 +115,15 @@ impl TextEditor {
|
|||||||
if let Some(active_tab) = self.get_active_tab() {
|
if let Some(active_tab) = self.get_active_tab() {
|
||||||
let content = &active_tab.content;
|
let content = &active_tab.content;
|
||||||
|
|
||||||
let start_char = Self::safe_slice_to_pos(content, start_byte).chars().count();
|
let start_char = safe_slice_to_pos(content, start_byte).chars().count();
|
||||||
let end_char = Self::safe_slice_to_pos(content, end_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) {
|
if let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) {
|
||||||
let selection_range = egui::text::CCursorRange::two(
|
let selection_range = egui::text::CCursorRange::two(
|
||||||
egui::text::CCursor::new(start_char),
|
egui::text::CCursor::new(start_char),
|
||||||
@ -152,12 +158,16 @@ impl TextEditor {
|
|||||||
self.update_find_matches();
|
self.update_find_matches();
|
||||||
|
|
||||||
if let Some(active_tab) = self.get_active_tab() {
|
if let Some(active_tab) = self.get_active_tab() {
|
||||||
let replacement_end_char =
|
let replacement_end_char = safe_slice_to_pos(&active_tab.content, replacement_end)
|
||||||
Self::safe_slice_to_pos(&active_tab.content, replacement_end)
|
.chars()
|
||||||
.chars()
|
.count();
|
||||||
.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) {
|
if let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) {
|
||||||
state
|
state
|
||||||
.cursor
|
.cursor
|
||||||
@ -206,14 +216,22 @@ impl TextEditor {
|
|||||||
|
|
||||||
self.current_match_index = None;
|
self.current_match_index = None;
|
||||||
|
|
||||||
let text_edit_id = egui::Id::new("main_text_editor");
|
if let Some(active_tab) = self.get_active_tab() {
|
||||||
if let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) {
|
let id_source = active_tab
|
||||||
state
|
.file_path
|
||||||
.cursor
|
.as_ref()
|
||||||
.set_char_range(Some(egui::text::CCursorRange::one(
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
egui::text::CCursor::new(0),
|
.unwrap_or_else(|| "untitled".to_string());
|
||||||
)));
|
let text_edit_id = egui::Id::new("main_text_editor").with(&id_source);
|
||||||
egui::TextEdit::store_state(ctx, text_edit_id, state);
|
|
||||||
|
if let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) {
|
||||||
|
state
|
||||||
|
.cursor
|
||||||
|
.set_char_range(Some(egui::text::CCursorRange::one(
|
||||||
|
egui::text::CCursor::new(0),
|
||||||
|
)));
|
||||||
|
egui::TextEdit::store_state(ctx, text_edit_id, state);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,8 @@
|
|||||||
use super::editor::{TextEditor, TextProcessingResult};
|
use super::editor::{TextEditor, TextProcessingResult};
|
||||||
|
use crate::util::safe_slice_to_pos;
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
|
|
||||||
impl TextEditor {
|
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) {
|
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;
|
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 font_id = self.get_font_id();
|
||||||
let longest_line_pixel_width = if longest_line_length > 0 {
|
let longest_line_pixel_width = if longest_line_length > 0 {
|
||||||
let longest_line_text = lines[longest_line_index];
|
let longest_line_text = lines[longest_line_index];
|
||||||
ui.fonts(|fonts| {
|
ui.fonts_mut(|fonts| {
|
||||||
fonts
|
fonts
|
||||||
.layout(
|
.layout_no_wrap(
|
||||||
longest_line_text.to_string(),
|
longest_line_text.to_string(),
|
||||||
font_id,
|
font_id,
|
||||||
egui::Color32::WHITE,
|
egui::Color32::WHITE,
|
||||||
f32::INFINITY,
|
|
||||||
)
|
)
|
||||||
.size()
|
.size()
|
||||||
.x
|
.x
|
||||||
@ -83,15 +74,6 @@ impl TextEditor {
|
|||||||
new_cursor_pos: usize,
|
new_cursor_pos: usize,
|
||||||
ui: &egui::Ui,
|
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() {
|
if old_content.len() == new_content.len() {
|
||||||
self.handle_character_replacement(
|
self.handle_character_replacement(
|
||||||
old_content,
|
old_content,
|
||||||
@ -128,12 +110,12 @@ impl TextEditor {
|
|||||||
old_cursor_pos: usize,
|
old_cursor_pos: usize,
|
||||||
new_cursor_pos: usize,
|
new_cursor_pos: usize,
|
||||||
) -> isize {
|
) -> 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()
|
.bytes()
|
||||||
.filter(|&b| b == b'\n')
|
.filter(|&b| b == b'\n')
|
||||||
.count();
|
.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()
|
.bytes()
|
||||||
.filter(|&b| b == b'\n')
|
.filter(|&b| b == b'\n')
|
||||||
.count();
|
.count();
|
||||||
@ -198,11 +180,11 @@ impl TextEditor {
|
|||||||
let mut current_result = self.get_text_processing_result();
|
let mut current_result = self.get_text_processing_result();
|
||||||
current_result.line_count += newlines_added;
|
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()
|
.bytes()
|
||||||
.filter(|&b| b == b'\n')
|
.filter(|&b| b == b'\n')
|
||||||
.count();
|
.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()
|
.bytes()
|
||||||
.filter(|&b| b == b'\n')
|
.filter(|&b| b == b'\n')
|
||||||
.count();
|
.count();
|
||||||
@ -268,11 +250,11 @@ impl TextEditor {
|
|||||||
let mut current_result = self.get_text_processing_result();
|
let mut current_result = self.get_text_processing_result();
|
||||||
current_result.line_count = current_result.line_count.saturating_sub(newlines_removed);
|
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()
|
.bytes()
|
||||||
.filter(|&b| b == b'\n')
|
.filter(|&b| b == b'\n')
|
||||||
.count();
|
.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()
|
.bytes()
|
||||||
.filter(|&b| b == b'\n')
|
.filter(|&b| b == b'\n')
|
||||||
.count();
|
.count();
|
||||||
@ -331,7 +313,7 @@ impl TextEditor {
|
|||||||
{
|
{
|
||||||
content[line_start_boundary..line_end_boundary].to_string()
|
content[line_start_boundary..line_end_boundary].to_string()
|
||||||
} else {
|
} 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 {
|
if line_length > current_result.longest_line_length {
|
||||||
let font_id = self.get_font_id();
|
let font_id = self.get_font_id();
|
||||||
let pixel_width = ui.fonts(|fonts| {
|
let pixel_width = ui.fonts_mut(|fonts| {
|
||||||
fonts
|
fonts
|
||||||
.layout(
|
.layout_no_wrap(
|
||||||
line_content.to_string(),
|
line_content.to_string(),
|
||||||
font_id,
|
font_id,
|
||||||
egui::Color32::WHITE,
|
egui::Color32::WHITE,
|
||||||
f32::INFINITY,
|
|
||||||
)
|
)
|
||||||
.size()
|
.size()
|
||||||
.x
|
.x
|
||||||
|
|||||||
@ -68,7 +68,7 @@ impl TextEditor {
|
|||||||
let font_id = self.get_font_id();
|
let font_id = self.get_font_id();
|
||||||
let line_count_digits = line_count.to_string().len();
|
let line_count_digits = line_count.to_string().len();
|
||||||
let sample_text = "9".repeat(line_count_digits);
|
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
|
fonts
|
||||||
.layout(sample_text, font_id, egui::Color32::WHITE, f32::INFINITY)
|
.layout(sample_text, font_id, egui::Color32::WHITE, f32::INFINITY)
|
||||||
.size()
|
.size()
|
||||||
@ -76,7 +76,7 @@ impl TextEditor {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let line_number_width = if self.line_side {
|
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 {
|
} else {
|
||||||
base_line_number_width
|
base_line_number_width
|
||||||
};
|
};
|
||||||
@ -95,10 +95,123 @@ impl TextEditor {
|
|||||||
return self.calculate_editor_dimensions(ui).text_width;
|
return self.calculate_editor_dimensions(ui).text_width;
|
||||||
}
|
}
|
||||||
|
|
||||||
let longest_line_width =
|
let longest_line_width = processing_result.longest_line_pixel_width;
|
||||||
processing_result.longest_line_pixel_width + (self.font_size * 3.0);
|
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);
|
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 app;
|
||||||
mod io;
|
mod io;
|
||||||
mod ui;
|
mod ui;
|
||||||
use app::{config::Config, TextEditor};
|
mod util;
|
||||||
|
use app::{TextEditor, config::Config};
|
||||||
|
|
||||||
fn main() -> eframe::Result {
|
fn main() -> eframe::Result {
|
||||||
let args: Vec<String> = env::args().collect();
|
let args: Vec<String> = env::args().collect();
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
pub(crate) mod about_window;
|
pub(crate) mod about_window;
|
||||||
|
pub(crate) mod bottom_bar;
|
||||||
pub(crate) mod central_panel;
|
pub(crate) mod central_panel;
|
||||||
pub(crate) mod constants;
|
pub(crate) mod constants;
|
||||||
pub(crate) mod find_window;
|
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 editor;
|
||||||
mod find_highlight;
|
mod find_highlight;
|
||||||
mod languages;
|
pub mod languages;
|
||||||
mod line_numbers;
|
mod line_numbers;
|
||||||
|
mod markdown;
|
||||||
|
|
||||||
use crate::app::TextEditor;
|
use crate::app::TextEditor;
|
||||||
use crate::ui::constants::*;
|
use crate::ui::constants::*;
|
||||||
@ -9,7 +10,16 @@ use eframe::egui;
|
|||||||
use egui::UiKind;
|
use egui::UiKind;
|
||||||
|
|
||||||
use self::editor::editor_view_ui;
|
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::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) {
|
pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||||
let show_line_numbers = app.show_line_numbers;
|
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 line_side = app.line_side;
|
||||||
let font_size = app.font_size;
|
let font_size = app.font_size;
|
||||||
let font_id = app.get_font_id();
|
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()
|
let _output = egui::CentralPanel::default()
|
||||||
.frame(egui::Frame::NONE)
|
.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);
|
ui.painter().rect_filled(panel_rect, 0.0, bg_color);
|
||||||
let editor_height = panel_rect.height();
|
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() {
|
if !show_line_numbers || app.get_active_tab().is_none() {
|
||||||
let _scroll_response =
|
let _scroll_response =
|
||||||
egui::ScrollArea::vertical()
|
egui::ScrollArea::vertical()
|
||||||
@ -35,46 +107,19 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
let context_response =
|
let context_response =
|
||||||
ui.allocate_response(full_rect.size(), egui::Sense::click());
|
ui.allocate_response(full_rect.size(), egui::Sense::click());
|
||||||
|
|
||||||
ui.scope_builder(egui::UiBuilder::new().max_rect(full_rect), |ui| {
|
let editor_response = ui
|
||||||
editor_view_ui(ui, app);
|
.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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let line_count = app.get_text_processing_result().line_count;
|
let line_count = app.get_text_processing_result().line_count;
|
||||||
let editor_dimensions = app.calculate_editor_dimensions(ui);
|
let line_number_width = app.calculate_editor_dimensions(ui).line_number_width;
|
||||||
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 separator_widget = |ui: &mut egui::Ui| {
|
let separator_widget = |ui: &mut egui::Ui| {
|
||||||
let separator_x = ui.cursor().left();
|
let separator_x = ui.cursor().left();
|
||||||
@ -88,13 +133,43 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
egui::ScrollArea::vertical()
|
egui::ScrollArea::vertical()
|
||||||
.auto_shrink([false; 2])
|
.auto_shrink([false; 2])
|
||||||
.show(ui, |ui| {
|
.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 {
|
if line_side {
|
||||||
ui.allocate_ui_with_layout(
|
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),
|
egui::Layout::left_to_right(egui::Align::TOP),
|
||||||
|ui| {
|
|ui| {
|
||||||
ui.allocate_ui_with_layout(
|
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),
|
egui::Layout::left_to_right(egui::Align::TOP),
|
||||||
|ui| {
|
|ui| {
|
||||||
let full_rect: egui::Rect = ui.available_rect_before_wrap();
|
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(),
|
egui::Sense::click(),
|
||||||
);
|
);
|
||||||
|
|
||||||
ui.scope_builder(
|
let editor_response = ui
|
||||||
egui::UiBuilder::new().max_rect(full_rect),
|
.scope_builder(
|
||||||
|ui| {
|
egui::UiBuilder::new().max_rect(full_rect),
|
||||||
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);
|
separator_widget(ui);
|
||||||
@ -119,7 +194,7 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
ui.allocate_ui_with_layout(
|
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),
|
egui::Layout::left_to_right(egui::Align::TOP),
|
||||||
|ui| {
|
|ui| {
|
||||||
line_numbers_widget(ui);
|
line_numbers_widget(ui);
|
||||||
@ -129,14 +204,14 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
let context_response =
|
let context_response =
|
||||||
ui.allocate_response(editor_area.size(), egui::Sense::click());
|
ui.allocate_response(editor_area.size(), egui::Sense::click());
|
||||||
|
|
||||||
ui.scope_builder(
|
let editor_response = ui
|
||||||
egui::UiBuilder::new().max_rect(editor_area),
|
.scope_builder(
|
||||||
|ui| {
|
egui::UiBuilder::new().max_rect(editor_area),
|
||||||
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() {
|
if context_response.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) {
|
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
||||||
if let Some(active_tab) = app.get_active_tab() {
|
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);
|
let cursor_pos = egui::text::CCursor::new(text_len);
|
||||||
state
|
state
|
||||||
.cursor
|
.cursor
|
||||||
.set_char_range(Some(egui::text::CCursorRange::one(cursor_pos)));
|
.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);
|
mem.request_focus(text_edit_id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context_response.context_menu(|ui| {
|
// Use the editor response for context menu so it captures right-clicks in the text area
|
||||||
let text_len = app.get_active_tab().unwrap().content.len();
|
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");
|
let reset_zoom_key = egui::Id::new("editor_reset_zoom");
|
||||||
|
|
||||||
if ui.button("Cut").clicked() {
|
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);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
if ui.button("Select All").clicked() {
|
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) {
|
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
||||||
let select_all_range = egui::text::CCursorRange::two(
|
let select_all_range = egui::text::CCursorRange::two(
|
||||||
egui::text::CCursor::new(0),
|
egui::text::CCursor::new(0),
|
||||||
|
|||||||
@ -5,16 +5,16 @@ use egui_extras::syntax_highlighting::{self};
|
|||||||
use super::find_highlight;
|
use super::find_highlight;
|
||||||
|
|
||||||
pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::Response {
|
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 show_find = app.show_find;
|
||||||
let _prev_show_find = app.prev_show_find;
|
|
||||||
let show_preferences = app.show_preferences;
|
let show_preferences = app.show_preferences;
|
||||||
let show_about = app.show_about;
|
let show_about = app.show_about;
|
||||||
let show_shortcuts = app.show_shortcuts;
|
let show_shortcuts = app.show_shortcuts;
|
||||||
|
|
||||||
let word_wrap = app.word_wrap;
|
let word_wrap = app.word_wrap;
|
||||||
let font_size = app.font_size;
|
let font_size = app.font_size;
|
||||||
let font_id = app.get_font_id();
|
let font_id = app.get_font_id();
|
||||||
let syntax_highlighting_enabled = app.syntax_highlighting;
|
let syntax_highlighting_enabled = app.syntax_highlighting;
|
||||||
|
let previous_cursor_position = app.previous_cursor_position;
|
||||||
|
|
||||||
let bg_color = ui.visuals().extreme_bg_color;
|
let bg_color = ui.visuals().extreme_bg_color;
|
||||||
let editor_rect = ui.available_rect_before_wrap();
|
let editor_rect = ui.available_rect_before_wrap();
|
||||||
@ -55,30 +55,34 @@ 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?");
|
return ui.label("No file open, how did you get here?");
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some((content, matches, current_match_index)) = &find_data {
|
let draw_highlights = |ui: &mut egui::Ui| {
|
||||||
let temp_galley = ui.fonts(|fonts| {
|
if let Some((content, matches, current_match_index)) = &find_data {
|
||||||
fonts.layout(
|
let temp_galley = ui.fonts_mut(|fonts| {
|
||||||
content.to_owned(),
|
fonts.layout(
|
||||||
font_id.to_owned(),
|
content.to_owned(),
|
||||||
ui.visuals().text_color(),
|
font_id.to_owned(),
|
||||||
desired_width,
|
ui.visuals().text_color(),
|
||||||
)
|
desired_width - 8.0,
|
||||||
});
|
)
|
||||||
|
});
|
||||||
|
|
||||||
let text_area_left = editor_rect.left() + 4.0; // Text Editor default margins
|
// Use the current cursor position which handles scroll offsets correctly
|
||||||
let text_area_top = editor_rect.top() + 2.0;
|
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(
|
find_highlight::draw_find_highlights(
|
||||||
ui,
|
ui,
|
||||||
content,
|
content,
|
||||||
matches,
|
matches,
|
||||||
*current_match_index,
|
*current_match_index,
|
||||||
&temp_galley,
|
&temp_galley,
|
||||||
text_area_left,
|
text_area_left,
|
||||||
text_area_top,
|
text_area_top,
|
||||||
font_size,
|
font_size,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let language = super::languages::get_language_from_extension(active_tab.file_path.as_deref());
|
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| {
|
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;
|
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)
|
let text_edit = egui::TextEdit::multiline(&mut active_tab.content)
|
||||||
.frame(false)
|
.frame(false)
|
||||||
.code_editor()
|
.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)
|
.lock_focus(!show_find)
|
||||||
.cursor_at_end(false)
|
.cursor_at_end(false)
|
||||||
.layouter(&mut layouter)
|
.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 {
|
let output = if word_wrap {
|
||||||
|
draw_highlights(ui);
|
||||||
text_edit.show(ui)
|
text_edit.show(ui)
|
||||||
} else {
|
} else {
|
||||||
egui::ScrollArea::horizontal()
|
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(
|
ui.allocate_ui_with_layout(
|
||||||
egui::Vec2::new(estimated_width, ui.available_height()),
|
egui::Vec2::new(estimated_width, ui.available_height()),
|
||||||
egui::Layout::left_to_right(egui::Align::TOP),
|
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
|
||||||
.inner
|
.inner
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ensure_cursor_visible(ui, &output, &font_id);
|
||||||
|
|
||||||
let content_changed = output.response.changed();
|
let content_changed = output.response.changed();
|
||||||
let content_for_processing = if content_changed {
|
let content_for_processing = if content_changed {
|
||||||
active_tab.update_modified_state();
|
active_tab.update_modified_state();
|
||||||
@ -139,10 +196,8 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
if content_changed {
|
if content_changed && let Err(e) = app.save_state_cache() {
|
||||||
if let Err(e) = app.save_state_cache() {
|
eprintln!("Failed to save state cache: {e}");
|
||||||
eprintln!("Failed to save state cache: {e}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if content_changed && app.show_find && !app.find_query.is_empty() {
|
if content_changed && app.show_find && !app.find_query.is_empty() {
|
||||||
@ -189,39 +244,18 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(cursor_pos) = current_cursor_pos {
|
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))
|
|
||||||
.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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
app.previous_cursor_position = Some(cursor_pos);
|
app.previous_cursor_position = Some(cursor_pos);
|
||||||
|
app.current_cursor_index = cursor_pos;
|
||||||
|
|
||||||
|
// Calculate line and column
|
||||||
|
if let Some(active_tab) = app.get_active_tab() {
|
||||||
|
let content = &active_tab.content;
|
||||||
|
let safe_pos = cursor_pos.min(content.len());
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !output.response.has_focus()
|
if !output.response.has_focus()
|
||||||
|
|||||||
@ -1,15 +1,6 @@
|
|||||||
|
use crate::util::safe_slice_to_pos;
|
||||||
use eframe::egui;
|
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(
|
pub(super) fn draw_find_highlights(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
content: &str,
|
content: &str,
|
||||||
@ -18,26 +9,18 @@ pub(super) fn draw_find_highlights(
|
|||||||
galley: &std::sync::Arc<egui::Galley>,
|
galley: &std::sync::Arc<egui::Galley>,
|
||||||
text_area_left: f32,
|
text_area_left: f32,
|
||||||
text_area_top: f32,
|
text_area_top: f32,
|
||||||
font_size: f32,
|
_font_size: f32,
|
||||||
) {
|
) {
|
||||||
let font_id = ui
|
for (match_index, &(start_byte, end_byte)) in matches.iter().enumerate() {
|
||||||
.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() {
|
|
||||||
let is_current_match = current_match_index == Some(match_index);
|
let is_current_match = current_match_index == Some(match_index);
|
||||||
draw_single_highlight(
|
draw_single_highlight(
|
||||||
ui,
|
ui,
|
||||||
content,
|
content,
|
||||||
start_pos,
|
start_byte,
|
||||||
end_pos,
|
end_byte,
|
||||||
|
galley,
|
||||||
text_area_left,
|
text_area_left,
|
||||||
text_area_top,
|
text_area_top,
|
||||||
galley,
|
|
||||||
&font_id,
|
|
||||||
is_current_match,
|
is_current_match,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -46,70 +29,15 @@ pub(super) fn draw_find_highlights(
|
|||||||
fn draw_single_highlight(
|
fn draw_single_highlight(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
content: &str,
|
content: &str,
|
||||||
start_pos: usize,
|
start_byte: usize,
|
||||||
end_pos: usize,
|
end_byte: usize,
|
||||||
|
galley: &std::sync::Arc<egui::Galley>,
|
||||||
text_area_left: f32,
|
text_area_left: f32,
|
||||||
text_area_top: f32,
|
text_area_top: f32,
|
||||||
galley: &std::sync::Arc<egui::Galley>,
|
|
||||||
font_id: &egui::FontId,
|
|
||||||
is_current_match: bool,
|
is_current_match: bool,
|
||||||
) {
|
) {
|
||||||
let text_up_to_start = safe_slice_to_pos(content, start_pos);
|
let start_char = safe_slice_to_pos(content, start_byte).chars().count();
|
||||||
let start_line = text_up_to_start.chars().filter(|&c| c == '\n').count();
|
let end_char = safe_slice_to_pos(content, end_byte).chars().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 highlight_color = if is_current_match {
|
let highlight_color = if is_current_match {
|
||||||
ui.visuals().selection.bg_fill
|
ui.visuals().selection.bg_fill
|
||||||
@ -118,5 +46,36 @@ fn draw_single_highlight(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let painter = ui.painter();
|
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 {
|
fn format_line_number(line_number: usize, line_side: bool, line_count_width: usize) -> String {
|
||||||
if line_side {
|
if line_side {
|
||||||
format!("{:<width$}", line_number, width = line_count_width)
|
// Right side: left-align with trailing space for scrollbar clearance
|
||||||
|
format!("{:<width$} ", line_number, width = line_count_width)
|
||||||
} else {
|
} else {
|
||||||
|
// Left side: right-align, no trailing space (separator provides gap)
|
||||||
format!("{:>width$}", line_number, width = line_count_width)
|
format!("{:>width$}", line_number, width = line_count_width)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -22,12 +24,12 @@ pub(super) fn calculate_visual_line_mapping(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let galley = ui.fonts(|fonts| {
|
let galley = ui.fonts_mut(|fonts| {
|
||||||
fonts.layout(
|
fonts.layout(
|
||||||
line.to_string(),
|
line.to_string(),
|
||||||
font_id.to_owned(),
|
font_id.to_owned(),
|
||||||
egui::Color32::WHITE,
|
egui::Color32::WHITE,
|
||||||
available_width - font_id.size,
|
available_width,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -60,7 +62,7 @@ pub(super) fn render_line_numbers(
|
|||||||
ui.disable();
|
ui.disable();
|
||||||
ui.set_width(line_number_width);
|
ui.set_width(line_number_width);
|
||||||
ui.spacing_mut().item_spacing.y = 0.0;
|
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 text_color = ui.visuals().weak_text_color();
|
||||||
let bg_color = ui.visuals().extreme_bg_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 PREVIEW_AREA_MAX_HEIGHT: f32 = 150.0;
|
||||||
|
|
||||||
pub const INNER_MARGIN: i8 = 8;
|
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(
|
let response = ui.add(
|
||||||
egui::TextEdit::singleline(&mut app.find_query)
|
egui::TextEdit::singleline(&mut app.find_query)
|
||||||
.desired_width(250.0)
|
.desired_width(250.0)
|
||||||
.hint_text("Enter search text..."),
|
.hint_text("Search..."),
|
||||||
);
|
);
|
||||||
|
|
||||||
if response.changed() {
|
if response.changed() {
|
||||||
@ -84,7 +84,7 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
let _replace_response = ui.add(
|
let _replace_response = ui.add(
|
||||||
egui::TextEdit::singleline(&mut app.replace_query)
|
egui::TextEdit::singleline(&mut app.replace_query)
|
||||||
.desired_width(250.0)
|
.desired_width(250.0)
|
||||||
.hint_text("Enter replacement text..."),
|
.hint_text("Replace..."),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -103,71 +103,145 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
ui.close_kind(UiKind::Menu);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
if ui.button("Select All").clicked() {
|
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() {
|
||||||
if let Some(mut state) =
|
let id_source = active_tab
|
||||||
egui::TextEdit::load_state(ui.ctx(), text_edit_id)
|
.file_path
|
||||||
{
|
.as_ref()
|
||||||
if let Some(active_tab) = app.get_active_tab() {
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
let text_len = active_tab.content.len();
|
.unwrap_or_else(|| "untitled".to_string());
|
||||||
let select_all_range = egui::text::CCursorRange::two(
|
let text_edit_id =
|
||||||
egui::text::CCursor::new(0),
|
egui::Id::new("main_text_editor").with(&id_source);
|
||||||
egui::text::CCursor::new(text_len),
|
if let Some(mut state) =
|
||||||
);
|
egui::TextEdit::load_state(ui.ctx(), text_edit_id)
|
||||||
state.cursor.set_char_range(Some(select_all_range));
|
{
|
||||||
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
if let Some(active_tab) = app.get_active_tab() {
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
state.cursor.set_char_range(Some(select_all_range));
|
||||||
|
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if ui.button("Undo").clicked() {
|
// Check if undo is available
|
||||||
let text_edit_id = egui::Id::new("main_text_editor");
|
let can_undo = if let Some(active_tab) = app.get_active_tab() {
|
||||||
if let Some(mut state) =
|
let id_source = active_tab
|
||||||
egui::TextEdit::load_state(ui.ctx(), text_edit_id)
|
.file_path
|
||||||
{
|
.as_ref()
|
||||||
if let Some(active_tab) = app.get_active_tab_mut() {
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
let current_state = (
|
.unwrap_or_else(|| "untitled".to_string());
|
||||||
state.cursor.char_range().unwrap_or_default(),
|
let text_edit_id = egui::Id::new("main_text_editor").with(&id_source);
|
||||||
active_tab.content.to_string(),
|
if let Some(state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
||||||
);
|
let current_state = (
|
||||||
let mut undoer = state.undoer();
|
state.cursor.char_range().unwrap_or_default(),
|
||||||
if let Some((cursor_range, content)) =
|
active_tab.content.to_string(),
|
||||||
undoer.undo(¤t_state)
|
);
|
||||||
{
|
state.undoer().undo(¤t_state).is_some()
|
||||||
active_tab.content = content.to_string();
|
} else {
|
||||||
state.cursor.set_char_range(Some(*cursor_range));
|
false
|
||||||
state.set_undoer(undoer);
|
}
|
||||||
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
} else {
|
||||||
active_tab.update_modified_state();
|
false
|
||||||
if app.show_find && !app.find_query.is_empty() {
|
};
|
||||||
app.update_find_matches();
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if let Some(active_tab) = app.get_active_tab_mut() {
|
||||||
|
let current_state = (
|
||||||
|
state.cursor.char_range().unwrap_or_default(),
|
||||||
|
active_tab.content.to_string(),
|
||||||
|
);
|
||||||
|
let mut undoer = state.undoer();
|
||||||
|
if let Some((cursor_range, content)) =
|
||||||
|
undoer.undo(¤t_state)
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
active_tab.update_modified_state();
|
||||||
|
if app.show_find && !app.find_query.is_empty() {
|
||||||
|
app.update_find_matches();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
if ui.button("Redo").clicked() {
|
// Check if redo is available
|
||||||
let text_edit_id = egui::Id::new("main_text_editor");
|
let can_redo = if let Some(active_tab) = app.get_active_tab() {
|
||||||
if let Some(mut state) =
|
let id_source = active_tab
|
||||||
egui::TextEdit::load_state(ui.ctx(), text_edit_id)
|
.file_path
|
||||||
{
|
.as_ref()
|
||||||
if let Some(active_tab) = app.get_active_tab_mut() {
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
let current_state = (
|
.unwrap_or_else(|| "untitled".to_string());
|
||||||
state.cursor.char_range().unwrap_or_default(),
|
let text_edit_id = egui::Id::new("main_text_editor").with(&id_source);
|
||||||
active_tab.content.to_string(),
|
if let Some(state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
||||||
);
|
let current_state = (
|
||||||
let mut undoer = state.undoer();
|
state.cursor.char_range().unwrap_or_default(),
|
||||||
if let Some((cursor_range, content)) =
|
active_tab.content.to_string(),
|
||||||
undoer.redo(¤t_state)
|
);
|
||||||
{
|
state.undoer().redo(¤t_state).is_some()
|
||||||
active_tab.content = content.to_string();
|
} else {
|
||||||
state.cursor.set_char_range(Some(*cursor_range));
|
false
|
||||||
state.set_undoer(undoer);
|
}
|
||||||
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
} else {
|
||||||
active_tab.update_modified_state();
|
false
|
||||||
if app.show_find && !app.find_query.is_empty() {
|
};
|
||||||
app.update_find_matches();
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if let Some(active_tab) = app.get_active_tab_mut() {
|
||||||
|
let current_state = (
|
||||||
|
state.cursor.char_range().unwrap_or_default(),
|
||||||
|
active_tab.content.to_string(),
|
||||||
|
);
|
||||||
|
let mut undoer = state.undoer();
|
||||||
|
if let Some((cursor_range, content)) =
|
||||||
|
undoer.redo(¤t_state)
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
active_tab.update_modified_state();
|
||||||
|
if app.show_find && !app.find_query.is_empty() {
|
||||||
|
app.update_find_matches();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -192,6 +266,13 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
app.save_config();
|
app.save_config();
|
||||||
ui.close_kind(UiKind::Menu);
|
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() {
|
if ui.checkbox(&mut app.word_wrap, "Word Wrap").clicked() {
|
||||||
app.save_config();
|
app.save_config();
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_kind(UiKind::Menu);
|
||||||
@ -200,6 +281,10 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
app.save_config();
|
app.save_config();
|
||||||
ui.close_kind(UiKind::Menu);
|
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
|
if ui
|
||||||
.checkbox(&mut app.auto_hide_toolbar, "Auto Hide Toolbar")
|
.checkbox(&mut app.auto_hide_toolbar, "Auto Hide Toolbar")
|
||||||
.clicked()
|
.clicked()
|
||||||
@ -289,10 +374,10 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
empty_tab.get_display_title()
|
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 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(
|
fonts.layout_job(egui::text::LayoutJob::simple_singleline(
|
||||||
tab_title,
|
tab_title,
|
||||||
font_id,
|
font_id,
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use eframe::egui;
|
|||||||
|
|
||||||
pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||||
let visuals = &ctx.style().visuals;
|
let visuals = &ctx.style().visuals;
|
||||||
let screen_rect = ctx.screen_rect();
|
let screen_rect = ctx.viewport_rect();
|
||||||
let window_width =
|
let window_width =
|
||||||
(screen_rect.width() * WINDOW_WIDTH_RATIO).clamp(WINDOW_MIN_WIDTH, WINDOW_MAX_WIDTH);
|
(screen_rect.width() * WINDOW_WIDTH_RATIO).clamp(WINDOW_MIN_WIDTH, WINDOW_MAX_WIDTH);
|
||||||
let window_height =
|
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 + 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 + 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 + 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 + =/-: Increase/Decrease Font Size").size(UI_TEXT_SIZE));
|
||||||
ui.label(egui::RichText::new("Ctrl + Shift + =/-: Zoom In/Out").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) {
|
pub(crate) fn shortcuts_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||||
let visuals = &ctx.style().visuals;
|
let visuals = &ctx.style().visuals;
|
||||||
let screen_rect = ctx.screen_rect();
|
let screen_rect = ctx.viewport_rect();
|
||||||
|
|
||||||
let window_width =
|
let window_width =
|
||||||
(screen_rect.width() * WINDOW_WIDTH_RATIO).clamp(WINDOW_MIN_WIDTH, WINDOW_MAX_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