markdown, better wrapping

This commit is contained in:
candle 2025-12-14 19:38:57 -05:00
parent a3158129d1
commit d2fb8bf8ed
22 changed files with 726 additions and 332 deletions

View File

@ -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" }

View File

@ -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;

View File

@ -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 {

View File

@ -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,

View File

@ -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,

View File

@ -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)
.chars()
.count();
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,14 +216,22 @@ impl TextEditor {
self.current_match_index = None;
let text_edit_id = egui::Id::new("main_text_editor");
if let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) {
state
.cursor
.set_char_range(Some(egui::text::CCursorRange::one(
egui::text::CCursor::new(0),
)));
egui::TextEdit::store_state(ctx, text_edit_id, state);
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
.set_char_range(Some(egui::text::CCursorRange::one(
egui::text::CCursor::new(0),
)));
egui::TextEdit::store_state(ctx, text_edit_id, state);
}
}
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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();

View File

@ -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
View 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));
});
});
});
}
}

View File

@ -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(
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);
},
);
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(
egui::UiBuilder::new().max_rect(editor_area),
|ui| {
editor_view_ui(ui, app);
},
);
let editor_response = ui
.scope_builder(
egui::UiBuilder::new().max_rect(editor_area),
|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),

View File

@ -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,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?");
};
if let Some((content, matches, current_match_index)) = &find_data {
let temp_galley = ui.fonts(|fonts| {
fonts.layout(
content.to_owned(),
font_id.to_owned(),
ui.visuals().text_color(),
desired_width,
)
});
let draw_highlights = |ui: &mut egui::Ui| {
if let Some((content, matches, current_match_index)) = &find_data {
let temp_galley = ui.fonts_mut(|fonts| {
fonts.layout(
content.to_owned(),
font_id.to_owned(),
ui.visuals().text_color(),
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,
content,
matches,
*current_match_index,
&temp_galley,
text_area_left,
text_area_top,
font_size,
);
}
find_highlight::draw_find_highlights(
ui,
content,
matches,
*current_match_index,
&temp_galley,
text_area_left,
text_area_top,
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,10 +196,8 @@ 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() {
eprintln!("Failed to save state cache: {e}");
}
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() {
@ -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 {
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.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()

View File

@ -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);
}
}

View File

@ -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 {
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 {
// 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;
@ -85,7 +87,7 @@ pub(super) fn render_line_numbers(
.map(|i| format_line_number(i, line_side, line_count_width))
.collect::<Vec<_>>()
};
for text in line_texts {
ui.label(
egui::RichText::new(text)

View 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);
}

View File

@ -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;

View File

@ -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..."),
);
});
}

View File

@ -103,71 +103,145 @@ 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(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 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);
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.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.separator();
if ui.button("Undo").clicked() {
let text_edit_id = egui::Id::new("main_text_editor");
if let Some(mut state) =
egui::TextEdit::load_state(ui.ctx(), text_edit_id)
{
if let Some(active_tab) = app.get_active_tab_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(&current_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();
// 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(&current_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)
{
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(&current_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);
}
if ui.button("Redo").clicked() {
let text_edit_id = egui::Id::new("main_text_editor");
if let Some(mut state) =
egui::TextEdit::load_state(ui.ctx(), text_edit_id)
{
if let Some(active_tab) = app.get_active_tab_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(&current_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();
// 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(&current_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)
{
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(&current_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();
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,

View File

@ -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 =

View File

@ -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
View 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]
}