2025-07-05 14:42:45 -04:00
|
|
|
use crate::app::TextEditor;
|
|
|
|
|
use eframe::egui;
|
|
|
|
|
|
|
|
|
|
use super::find_highlight::draw_find_highlight;
|
|
|
|
|
|
|
|
|
|
pub(super) fn editor_view(
|
|
|
|
|
ui: &mut egui::Ui,
|
|
|
|
|
app: &mut TextEditor,
|
|
|
|
|
) -> (egui::Response, Option<egui::Rect>) {
|
|
|
|
|
let current_match_position = app.get_current_match_position();
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
// Check if reset zoom was requested in previous frame
|
|
|
|
|
let reset_zoom_key = egui::Id::new("editor_reset_zoom");
|
|
|
|
|
let should_reset_zoom = ui.ctx().memory_mut(|mem| {
|
|
|
|
|
mem.data.get_temp::<bool>(reset_zoom_key).unwrap_or(false)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Reset zoom if requested
|
|
|
|
|
if should_reset_zoom {
|
|
|
|
|
app.zoom_factor = 1.0;
|
|
|
|
|
ui.ctx().set_zoom_factor(1.0);
|
|
|
|
|
ui.ctx().memory_mut(|mem| {
|
|
|
|
|
mem.data.insert_temp(reset_zoom_key, false);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(active_tab) = app.get_active_tab_mut() {
|
|
|
|
|
let bg_color = ui.visuals().extreme_bg_color;
|
|
|
|
|
let editor_rect = ui.available_rect_before_wrap();
|
|
|
|
|
ui.painter().rect_filled(editor_rect, 0.0, bg_color);
|
|
|
|
|
|
|
|
|
|
let desired_width = if word_wrap {
|
|
|
|
|
ui.available_width()
|
|
|
|
|
} else {
|
|
|
|
|
f32::INFINITY
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let text_edit = egui::TextEdit::multiline(&mut active_tab.content)
|
|
|
|
|
.frame(false)
|
|
|
|
|
.font(egui::TextStyle::Monospace)
|
|
|
|
|
.code_editor()
|
|
|
|
|
.desired_width(desired_width)
|
|
|
|
|
.desired_rows(0)
|
|
|
|
|
.lock_focus(true)
|
|
|
|
|
.cursor_at_end(false)
|
|
|
|
|
.id(egui::Id::new("main_text_editor"));
|
|
|
|
|
|
|
|
|
|
let output = text_edit.show(ui);
|
|
|
|
|
|
|
|
|
|
// Store text length for context menu
|
|
|
|
|
let text_len = active_tab.content.len();
|
|
|
|
|
|
|
|
|
|
// Right-click context menu
|
|
|
|
|
output.response.context_menu(|ui| {
|
|
|
|
|
if ui.button("Cut").clicked() {
|
|
|
|
|
ui.ctx().input_mut(|i| i.events.push(egui::Event::Cut));
|
|
|
|
|
ui.close_menu();
|
|
|
|
|
}
|
|
|
|
|
if ui.button("Copy").clicked() {
|
|
|
|
|
ui.ctx().input_mut(|i| i.events.push(egui::Event::Copy));
|
|
|
|
|
ui.close_menu();
|
|
|
|
|
}
|
|
|
|
|
if ui.button("Paste").clicked() {
|
|
|
|
|
ui.ctx()
|
|
|
|
|
.send_viewport_cmd(egui::ViewportCommand::RequestPaste);
|
|
|
|
|
ui.close_menu();
|
|
|
|
|
}
|
|
|
|
|
if ui.button("Delete").clicked() {
|
|
|
|
|
ui.ctx().input_mut(|i| {
|
|
|
|
|
i.events.push(egui::Event::Key {
|
|
|
|
|
key: egui::Key::Delete,
|
|
|
|
|
physical_key: None,
|
|
|
|
|
pressed: true,
|
|
|
|
|
repeat: false,
|
|
|
|
|
modifiers: egui::Modifiers::NONE,
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
ui.close_menu();
|
|
|
|
|
}
|
|
|
|
|
if ui.button("Select All").clicked() {
|
|
|
|
|
let text_edit_id = egui::Id::new("main_text_editor");
|
|
|
|
|
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
|
|
|
|
let select_all_range = egui::text::CCursorRange::two(
|
|
|
|
|
egui::text::CCursor::new(0),
|
|
|
|
|
egui::text::CCursor::new(text_len),
|
|
|
|
|
);
|
|
|
|
|
state.cursor.set_char_range(Some(select_all_range));
|
|
|
|
|
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
|
|
|
|
}
|
|
|
|
|
ui.close_menu();
|
|
|
|
|
}
|
|
|
|
|
ui.separator();
|
|
|
|
|
if ui.button("Reset Zoom").clicked() {
|
|
|
|
|
ui.ctx().memory_mut(|mem| {
|
|
|
|
|
mem.data.insert_temp(reset_zoom_key, true);
|
|
|
|
|
});
|
|
|
|
|
ui.close_menu();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let cursor_rect = if let Some(cursor_range) = output.state.cursor.char_range() {
|
|
|
|
|
let cursor_pos = cursor_range.primary.index;
|
|
|
|
|
let content = &active_tab.content;
|
|
|
|
|
|
2025-07-09 17:30:03 -04:00
|
|
|
// Count newlines up to cursor position using char_indices to avoid char boundary issues
|
|
|
|
|
let cursor_line = content
|
|
|
|
|
.char_indices()
|
|
|
|
|
.take_while(|(byte_pos, _)| *byte_pos < cursor_pos)
|
|
|
|
|
.filter(|(_, ch)| *ch == '\n')
|
|
|
|
|
.count();
|
2025-07-05 14:42:45 -04:00
|
|
|
|
|
|
|
|
let font_id = ui
|
|
|
|
|
.style()
|
|
|
|
|
.text_styles
|
|
|
|
|
.get(&egui::TextStyle::Monospace)
|
|
|
|
|
.unwrap_or(&egui::FontId::monospace(font_size))
|
|
|
|
|
.clone();
|
|
|
|
|
let line_height = ui.fonts(|fonts| fonts.row_height(&font_id));
|
|
|
|
|
|
|
|
|
|
let y_pos = output.response.rect.top() + (cursor_line as f32 * line_height);
|
|
|
|
|
let cursor_rect = egui::Rect::from_min_size(
|
|
|
|
|
egui::pos2(output.response.rect.left(), y_pos),
|
|
|
|
|
egui::vec2(2.0, line_height),
|
|
|
|
|
);
|
|
|
|
|
Some(cursor_rect)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if !show_find && prev_show_find {
|
|
|
|
|
if let Some((start_pos, end_pos)) = current_match_position {
|
|
|
|
|
let text_edit_id = egui::Id::new("main_text_editor");
|
|
|
|
|
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
|
|
|
|
let cursor_range = egui::text::CCursorRange::two(
|
|
|
|
|
egui::text::CCursor::new(start_pos),
|
|
|
|
|
egui::text::CCursor::new(end_pos),
|
|
|
|
|
);
|
|
|
|
|
state.cursor.set_char_range(Some(cursor_range));
|
|
|
|
|
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if show_find {
|
|
|
|
|
if let Some((start_pos, end_pos)) = current_match_position {
|
|
|
|
|
draw_find_highlight(
|
|
|
|
|
ui,
|
|
|
|
|
&active_tab.content,
|
|
|
|
|
start_pos,
|
|
|
|
|
end_pos,
|
|
|
|
|
output.response.rect,
|
|
|
|
|
font_size,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if output.response.changed() {
|
|
|
|
|
active_tab.update_modified_state();
|
|
|
|
|
app.find_matches.clear();
|
|
|
|
|
app.current_match_index = None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !output.response.has_focus()
|
|
|
|
|
&& !show_preferences
|
|
|
|
|
&& !show_about
|
|
|
|
|
&& !show_shortcuts
|
|
|
|
|
&& !show_find
|
|
|
|
|
{
|
|
|
|
|
output.response.request_focus();
|
|
|
|
|
}
|
|
|
|
|
(output.response, cursor_rect)
|
|
|
|
|
} else {
|
|
|
|
|
(ui.label("No file open, how did you get here?"), None)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) {
|
|
|
|
|
let word_wrap = app.word_wrap;
|
|
|
|
|
|
|
|
|
|
if word_wrap {
|
|
|
|
|
let (_response, _cursor_rect) = editor_view(ui, app);
|
|
|
|
|
} else {
|
|
|
|
|
let estimated_width = app.calculate_content_based_width(ui);
|
|
|
|
|
let output = egui::ScrollArea::horizontal()
|
|
|
|
|
.auto_shrink([false; 2])
|
|
|
|
|
.show(ui, |ui| {
|
|
|
|
|
ui.allocate_ui_with_layout(
|
|
|
|
|
egui::Vec2::new(estimated_width, ui.available_height()),
|
|
|
|
|
egui::Layout::left_to_right(egui::Align::TOP),
|
|
|
|
|
|ui| editor_view(ui, app),
|
|
|
|
|
)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let editor_response = &output.inner.inner.0;
|
|
|
|
|
if let Some(cursor_rect) = output.inner.inner.1 {
|
|
|
|
|
let text_edit_id = egui::Id::new("main_text_editor");
|
|
|
|
|
let current_cursor_pos =
|
|
|
|
|
if let Some(state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
|
|
|
|
state.cursor.char_range().map(|range| range.primary.index)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let cursor_moved = current_cursor_pos != app.previous_cursor_position;
|
|
|
|
|
let text_changed = editor_response.changed();
|
|
|
|
|
let should_scroll = (cursor_moved || text_changed)
|
|
|
|
|
&& {
|
|
|
|
|
let visible_area = ui.clip_rect();
|
|
|
|
|
!visible_area.intersects(cursor_rect)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if should_scroll {
|
|
|
|
|
ui.scroll_to_rect(cursor_rect, Some(egui::Align::Center));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
app.previous_cursor_position = current_cursor_pos;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|