Compare commits
3 Commits
a3158129d1
...
1dbfd4e233
| Author | SHA1 | Date | |
|---|---|---|---|
| 1dbfd4e233 | |||
| 90a438fb37 | |||
| d2fb8bf8ed |
12
Cargo.toml
12
Cargo.toml
@ -1,12 +1,12 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ced"
|
name = "ced"
|
||||||
version = "0.1.3"
|
version = "0.3.3"
|
||||||
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,7 @@ 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" }
|
||||||
|
egui_nerdfonts = "0.1.3"
|
||||||
|
vte = "0.13"
|
||||||
|
nix = { version = "0.29", features = ["term", "process", "fs"] }
|
||||||
|
|||||||
12
README.md
12
README.md
@ -10,6 +10,8 @@ There is a disturbing lack of simple GUI text editors available on Linux nativel
|
|||||||
|
|
||||||
* Sane text editing with standard keybindings (`Ctrl+A`, `Ctrl+C`, etc.).
|
* Sane text editing with standard keybindings (`Ctrl+A`, `Ctrl+C`, etc.).
|
||||||
* Choose between a fresh start each time you open, or maintaining a consistent state.
|
* Choose between a fresh start each time you open, or maintaining a consistent state.
|
||||||
|
* Built-in Markdown viewer.
|
||||||
|
* Toggleable file tree.
|
||||||
* Separate UI zoom that doesn't affect font size (`Ctrl+Shift` + `+`/`-`).
|
* Separate UI zoom that doesn't affect font size (`Ctrl+Shift` + `+`/`-`).
|
||||||
* Ricers rejoice, your `pywal` colors will be used!
|
* Ricers rejoice, your `pywal` colors will be used!
|
||||||
* Weirdly smooth typing experience.
|
* Weirdly smooth typing experience.
|
||||||
@ -22,7 +24,7 @@ There is a disturbing lack of simple GUI text editors available on Linux nativel
|
|||||||
##### Ubuntu/Debian
|
##### Ubuntu/Debian
|
||||||
`sudo apt install git rust`
|
`sudo apt install git rust`
|
||||||
|
|
||||||
#### Install
|
### Install
|
||||||
```bash
|
```bash
|
||||||
git clone https://code.lampnet.io/candle/ced
|
git clone https://code.lampnet.io/candle/ced
|
||||||
cd ced && cargo build --release
|
cd ced && cargo build --release
|
||||||
@ -32,7 +34,7 @@ sudo install -Dm644 ced.desktop /usr/share/applications/ced.desktop
|
|||||||
|
|
||||||
`ced` should now appear as 'Text Editor' in your application launcher. You can remove the cloned directory at this point.
|
`ced` should now appear as 'Text Editor' in your application launcher. You can remove the cloned directory at this point.
|
||||||
|
|
||||||
## Configuration
|
### Configuration
|
||||||
|
|
||||||
`ced` will look for, and create if needed, a configuration file at: `$XDG_CONFIG_HOME/ced/config.toml`.
|
`ced` will look for, and create if needed, a configuration file at: `$XDG_CONFIG_HOME/ced/config.toml`.
|
||||||
|
|
||||||
@ -45,6 +47,8 @@ show_line_numbers = false
|
|||||||
word_wrap = false
|
word_wrap = false
|
||||||
theme = "System"
|
theme = "System"
|
||||||
line_side = false
|
line_side = false
|
||||||
|
show_file_tree = true
|
||||||
|
file_tree_side = false
|
||||||
font_family = "Monospace"
|
font_family = "Monospace"
|
||||||
font_size = 16.0
|
font_size = 16.0
|
||||||
syntax_highlighting = true
|
syntax_highlighting = true
|
||||||
@ -56,10 +60,12 @@ syntax_highlighting = true
|
|||||||
|--------|---------|-------------|
|
|--------|---------|-------------|
|
||||||
| `state_cache` | `false` | If `true`, opened files will remain opened with their unsaved changes when running the application again. |
|
| `state_cache` | `false` | If `true`, opened files will remain opened with their unsaved changes when running the application again. |
|
||||||
| `auto_hide_toolbar` | `false` | If `true`, the menu bar at the top will be hidden. Move your mouse to the top of the window to reveal it. |
|
| `auto_hide_toolbar` | `false` | If `true`, the menu bar at the top will be hidden. Move your mouse to the top of the window to reveal it. |
|
||||||
| `hide_tab_bar` | 'true' | If `false`, a separate tab bar will be drawn below the toolbar. |
|
| `show_tab_bar` | 'true' | If `false`, a separate tab bar will be drawn below the toolbar. |
|
||||||
| `show_line_numbers` | `false` | If `true`, line numbers will be displayed on the side specified by `line_side`. |
|
| `show_line_numbers` | `false` | If `true`, line numbers will be displayed on the side specified by `line_side`. |
|
||||||
|
| `show_file_tree` | `false` | If `true`, a file tree will be displayed on the side specified by `file_tree_side`. |
|
||||||
| `syntax_highlighting` | `false` | If `true`, text will be highlighted based on detected language. |
|
| `syntax_highlighting` | `false` | If `true`, text will be highlighted based on detected language. |
|
||||||
| `line_side` | `false` | If `false`, line numbers are on the left. If `true`, they are on the right. |
|
| `line_side` | `false` | If `false`, line numbers are on the left. If `true`, they are on the right. |
|
||||||
|
| `file_tree_side` | `false` | If `false`, file tree will appear on the lift. If `true` it will appear on the right. |
|
||||||
| `word_wrap` | `false` | If `true`, lines will wrap when they reach the edge of the window. |
|
| `word_wrap` | `false` | If `true`, lines will wrap when they reach the edge of the window. |
|
||||||
| `font_family` | `"Proportional"` | The font family used for the editor text. |
|
| `font_family` | `"Proportional"` | The font family used for the editor text. |
|
||||||
| `font_size` | `14.0` | `8.0-32.0` The font size for text editing. |
|
| `font_size` | `14.0` | `8.0-32.0` The font size for text editing. |
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
pub mod actions;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod shortcuts;
|
pub mod shortcuts;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
|||||||
33
src/app/actions.rs
Normal file
33
src/app/actions.rs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ShortcutAction {
|
||||||
|
NewFile,
|
||||||
|
OpenFile,
|
||||||
|
SaveFile,
|
||||||
|
SaveAsFile,
|
||||||
|
NewTab,
|
||||||
|
CloseTab,
|
||||||
|
ToggleLineNumbers,
|
||||||
|
ToggleLineSide,
|
||||||
|
ToggleWordWrap,
|
||||||
|
ToggleAutoHideToolbar,
|
||||||
|
ToggleBottomBar,
|
||||||
|
ToggleFileTree,
|
||||||
|
ToggleFileTreeSide,
|
||||||
|
ToggleFind,
|
||||||
|
ToggleReplace,
|
||||||
|
ToggleMarkdown,
|
||||||
|
FocusFind,
|
||||||
|
NextTab,
|
||||||
|
PrevTab,
|
||||||
|
PageUp,
|
||||||
|
PageDown,
|
||||||
|
ZoomIn,
|
||||||
|
ZoomOut,
|
||||||
|
GlobalZoomIn,
|
||||||
|
GlobalZoomOut,
|
||||||
|
ResetZoom,
|
||||||
|
Escape,
|
||||||
|
Preferences,
|
||||||
|
ToggleVimMode,
|
||||||
|
ToggleFocusMode,
|
||||||
|
}
|
||||||
@ -4,72 +4,54 @@ use std::path::PathBuf;
|
|||||||
use super::theme::Theme;
|
use super::theme::Theme;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
#[serde(default = "default_state_cache")]
|
|
||||||
pub state_cache: bool,
|
pub state_cache: bool,
|
||||||
#[serde(default = "default_auto_hide_toolbar")]
|
|
||||||
pub auto_hide_toolbar: bool,
|
pub auto_hide_toolbar: bool,
|
||||||
#[serde(default = "default_hide_tab_bar")]
|
pub show_tab_bar: bool,
|
||||||
pub hide_tab_bar: bool,
|
pub show_bottom_bar: bool,
|
||||||
#[serde(default = "default_show_line_numbers")]
|
pub show_file_tree: bool,
|
||||||
pub show_line_numbers: bool,
|
pub show_line_numbers: bool,
|
||||||
#[serde(default = "default_word_wrap")]
|
|
||||||
pub word_wrap: bool,
|
pub word_wrap: bool,
|
||||||
#[serde(default = "Theme::default")]
|
|
||||||
pub theme: Theme,
|
pub theme: Theme,
|
||||||
#[serde(default = "default_line_side")]
|
|
||||||
pub line_side: bool,
|
pub line_side: bool,
|
||||||
#[serde(default = "default_font_family")]
|
pub file_tree_side: bool,
|
||||||
|
pub show_hidden_files: bool,
|
||||||
|
pub show_terminal: bool,
|
||||||
|
pub follow_git: bool,
|
||||||
|
pub tab_char: bool,
|
||||||
|
pub tab_width: usize,
|
||||||
pub font_family: String,
|
pub font_family: String,
|
||||||
#[serde(default = "default_font_size")]
|
|
||||||
pub font_size: f32,
|
pub font_size: f32,
|
||||||
#[serde(default = "default_syntax_highlighting")]
|
|
||||||
pub syntax_highlighting: bool,
|
pub syntax_highlighting: bool,
|
||||||
|
pub auto_indent: bool,
|
||||||
|
pub focus_mode: bool,
|
||||||
// pub vim_mode: bool,
|
// pub vim_mode: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_state_cache() -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_auto_hide_toolbar() -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
fn default_hide_tab_bar() -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
fn default_show_line_numbers() -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
fn default_word_wrap() -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
fn default_line_side() -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
fn default_font_family() -> String {
|
|
||||||
"Proportional".to_string()
|
|
||||||
}
|
|
||||||
fn default_font_size() -> f32 {
|
|
||||||
14.0
|
|
||||||
}
|
|
||||||
fn default_syntax_highlighting() -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
state_cache: default_state_cache(),
|
state_cache: false,
|
||||||
auto_hide_toolbar: default_auto_hide_toolbar(),
|
auto_hide_toolbar: false,
|
||||||
hide_tab_bar: default_hide_tab_bar(),
|
show_tab_bar: false,
|
||||||
show_line_numbers: default_show_line_numbers(),
|
show_bottom_bar: true,
|
||||||
word_wrap: default_word_wrap(),
|
show_file_tree: false,
|
||||||
|
show_line_numbers: false,
|
||||||
|
word_wrap: true,
|
||||||
theme: Theme::default(),
|
theme: Theme::default(),
|
||||||
line_side: default_line_side(),
|
line_side: false,
|
||||||
font_family: default_font_family(),
|
file_tree_side: false,
|
||||||
font_size: default_font_size(),
|
show_hidden_files: false,
|
||||||
syntax_highlighting: default_syntax_highlighting(),
|
show_terminal: false,
|
||||||
|
follow_git: true,
|
||||||
|
tab_char: false,
|
||||||
|
tab_width: 4,
|
||||||
|
font_family: "Proportional".to_string(),
|
||||||
|
font_size: 14.0,
|
||||||
|
syntax_highlighting: false,
|
||||||
|
auto_indent: true,
|
||||||
|
focus_mode: false,
|
||||||
// vim_mode: false,
|
// vim_mode: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,36 +1,7 @@
|
|||||||
|
use crate::app::actions::ShortcutAction;
|
||||||
use crate::app::state::TextEditor;
|
use crate::app::state::TextEditor;
|
||||||
use crate::io;
|
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
enum ShortcutAction {
|
|
||||||
NewFile,
|
|
||||||
OpenFile,
|
|
||||||
SaveFile,
|
|
||||||
SaveAsFile,
|
|
||||||
NewTab,
|
|
||||||
CloseTab,
|
|
||||||
ToggleLineNumbers,
|
|
||||||
ToggleLineSide,
|
|
||||||
ToggleWordWrap,
|
|
||||||
ToggleAutoHideToolbar,
|
|
||||||
ToggleFind,
|
|
||||||
ToggleReplace,
|
|
||||||
FocusFind,
|
|
||||||
NextTab,
|
|
||||||
PrevTab,
|
|
||||||
PageUp,
|
|
||||||
PageDown,
|
|
||||||
ZoomIn,
|
|
||||||
ZoomOut,
|
|
||||||
GlobalZoomIn,
|
|
||||||
GlobalZoomOut,
|
|
||||||
ResetZoom,
|
|
||||||
Escape,
|
|
||||||
Preferences,
|
|
||||||
ToggleVimMode,
|
|
||||||
}
|
|
||||||
|
|
||||||
type ShortcutDefinition = (egui::Modifiers, egui::Key, ShortcutAction);
|
type ShortcutDefinition = (egui::Modifiers, egui::Key, ShortcutAction);
|
||||||
|
|
||||||
fn get_shortcuts() -> Vec<ShortcutDefinition> {
|
fn get_shortcuts() -> Vec<ShortcutDefinition> {
|
||||||
@ -57,6 +28,11 @@ fn get_shortcuts() -> Vec<ShortcutDefinition> {
|
|||||||
egui::Key::W,
|
egui::Key::W,
|
||||||
ShortcutAction::CloseTab,
|
ShortcutAction::CloseTab,
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
egui::Modifiers::CTRL | egui::Modifiers::ALT,
|
||||||
|
egui::Key::F,
|
||||||
|
ShortcutAction::ToggleFocusMode,
|
||||||
|
),
|
||||||
(
|
(
|
||||||
egui::Modifiers::CTRL | egui::Modifiers::SHIFT,
|
egui::Modifiers::CTRL | egui::Modifiers::SHIFT,
|
||||||
egui::Key::F,
|
egui::Key::F,
|
||||||
@ -92,6 +68,21 @@ 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::Key::E,
|
||||||
|
ShortcutAction::ToggleFileTreeSide
|
||||||
|
),
|
||||||
|
(
|
||||||
|
egui::Modifiers::CTRL,
|
||||||
|
egui::Key::E,
|
||||||
|
ShortcutAction::ToggleFileTree,
|
||||||
|
),
|
||||||
(
|
(
|
||||||
egui::Modifiers::CTRL | egui::Modifiers::SHIFT,
|
egui::Modifiers::CTRL | egui::Modifiers::SHIFT,
|
||||||
egui::Key::Tab,
|
egui::Key::Tab,
|
||||||
@ -152,153 +143,23 @@ fn get_shortcuts() -> Vec<ShortcutDefinition> {
|
|||||||
egui::Key::Escape,
|
egui::Key::Escape,
|
||||||
ShortcutAction::Escape,
|
ShortcutAction::Escape,
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
egui::Modifiers::CTRL,
|
||||||
|
egui::Key::M,
|
||||||
|
ShortcutAction::ToggleMarkdown,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute_action(action: ShortcutAction, editor: &mut TextEditor) -> bool {
|
fn execute_action(action: ShortcutAction, editor: &mut TextEditor) -> bool {
|
||||||
match action {
|
editor.perform_action(action)
|
||||||
ShortcutAction::NewFile => {
|
|
||||||
io::new_file(editor);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
ShortcutAction::OpenFile => {
|
|
||||||
io::open_file(editor);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
ShortcutAction::SaveFile => {
|
|
||||||
io::save_file(editor);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
ShortcutAction::SaveAsFile => {
|
|
||||||
io::save_as_file(editor);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
ShortcutAction::NewTab => {
|
|
||||||
editor.add_new_tab();
|
|
||||||
false
|
|
||||||
}
|
|
||||||
ShortcutAction::CloseTab => {
|
|
||||||
if editor.tabs.len() > 1 {
|
|
||||||
if let Some(current_tab) = editor.get_active_tab() {
|
|
||||||
if current_tab.is_modified {
|
|
||||||
editor.pending_unsaved_action = Some(
|
|
||||||
super::state::UnsavedAction::CloseTab(editor.active_tab_index),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
editor.close_tab(editor.active_tab_index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
ShortcutAction::ToggleLineNumbers => {
|
|
||||||
editor.show_line_numbers = !editor.show_line_numbers;
|
|
||||||
editor.save_config();
|
|
||||||
false
|
|
||||||
}
|
|
||||||
ShortcutAction::ToggleLineSide => {
|
|
||||||
editor.line_side = !editor.line_side;
|
|
||||||
editor.save_config();
|
|
||||||
false
|
|
||||||
}
|
|
||||||
ShortcutAction::ToggleWordWrap => {
|
|
||||||
editor.word_wrap = !editor.word_wrap;
|
|
||||||
editor.save_config();
|
|
||||||
false
|
|
||||||
}
|
|
||||||
ShortcutAction::ToggleAutoHideToolbar => {
|
|
||||||
editor.auto_hide_toolbar = !editor.auto_hide_toolbar;
|
|
||||||
editor.save_config();
|
|
||||||
false
|
|
||||||
}
|
|
||||||
ShortcutAction::NextTab => {
|
|
||||||
let next_tab_index = editor.active_tab_index + 1;
|
|
||||||
if next_tab_index < editor.tabs.len() {
|
|
||||||
editor.switch_to_tab(next_tab_index);
|
|
||||||
} else {
|
|
||||||
editor.switch_to_tab(0);
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
ShortcutAction::PrevTab => {
|
|
||||||
if editor.active_tab_index == 0 {
|
|
||||||
editor.switch_to_tab(editor.tabs.len() - 1);
|
|
||||||
} else {
|
|
||||||
editor.switch_to_tab(editor.active_tab_index - 1);
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
ShortcutAction::PageUp => false,
|
|
||||||
ShortcutAction::PageDown => false,
|
|
||||||
ShortcutAction::ZoomIn => {
|
|
||||||
editor.font_size += 1.0;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
ShortcutAction::ZoomOut => {
|
|
||||||
editor.font_size -= 1.0;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
ShortcutAction::GlobalZoomIn => {
|
|
||||||
editor.zoom_factor += 0.1;
|
|
||||||
false
|
|
||||||
}
|
|
||||||
ShortcutAction::GlobalZoomOut => {
|
|
||||||
editor.zoom_factor -= 0.1;
|
|
||||||
if editor.zoom_factor < 0.1 {
|
|
||||||
editor.zoom_factor = 0.1;
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
ShortcutAction::ResetZoom => {
|
|
||||||
editor.zoom_factor = 1.0;
|
|
||||||
false
|
|
||||||
}
|
|
||||||
ShortcutAction::ToggleVimMode => {
|
|
||||||
// editor.vim_mode = !editor.vim_mode;
|
|
||||||
false
|
|
||||||
}
|
|
||||||
ShortcutAction::Escape => {
|
|
||||||
editor.show_about = false;
|
|
||||||
editor.show_shortcuts = false;
|
|
||||||
if editor.show_find {
|
|
||||||
editor.should_select_current_match = true;
|
|
||||||
}
|
|
||||||
editor.show_find = false;
|
|
||||||
editor.show_preferences = false;
|
|
||||||
editor.pending_unsaved_action = None;
|
|
||||||
false
|
|
||||||
}
|
|
||||||
ShortcutAction::ToggleFind => {
|
|
||||||
editor.show_find = !editor.show_find;
|
|
||||||
if editor.show_find && !editor.find_query.is_empty() {
|
|
||||||
editor.update_find_matches();
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
ShortcutAction::ToggleReplace => {
|
|
||||||
editor.show_find = !editor.show_find;
|
|
||||||
editor.show_replace_section = true;
|
|
||||||
if editor.show_find && !editor.find_query.is_empty() {
|
|
||||||
editor.update_find_matches();
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
ShortcutAction::FocusFind => {
|
|
||||||
if editor.show_find {
|
|
||||||
editor.focus_find = true;
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
ShortcutAction::Preferences => {
|
|
||||||
editor.show_preferences = !editor.show_preferences;
|
|
||||||
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 +174,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 +197,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,15 +1,21 @@
|
|||||||
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::file_tree::file_tree;
|
||||||
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;
|
||||||
use crate::ui::preferences_window::preferences_window;
|
use crate::ui::preferences_window::preferences_window;
|
||||||
use crate::ui::shortcuts_window::shortcuts_window;
|
use crate::ui::shortcuts_window::shortcuts_window;
|
||||||
use crate::ui::tab_bar::tab_bar;
|
use crate::ui::tab_bar::tab_bar;
|
||||||
|
use crate::ui::shell_bar::shell_bar;
|
||||||
|
|
||||||
impl eframe::App for TextEditor {
|
impl eframe::App for TextEditor {
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
|
// Reset focus manager at the start of each frame
|
||||||
|
self.focus_manager.reset();
|
||||||
|
|
||||||
if ctx.input(|i| i.viewport().close_requested())
|
if ctx.input(|i| i.viewport().close_requested())
|
||||||
&& !self.force_quit_confirmed
|
&& !self.force_quit_confirmed
|
||||||
&& !self.clean_quit_requested
|
&& !self.clean_quit_requested
|
||||||
@ -22,21 +28,35 @@ impl eframe::App for TextEditor {
|
|||||||
|
|
||||||
ctx.send_viewport_cmd(egui::ViewportCommand::Title(self.get_title()));
|
ctx.send_viewport_cmd(egui::ViewportCommand::Title(self.get_title()));
|
||||||
|
|
||||||
|
if !self.focus_mode {
|
||||||
menu_bar(self, ctx);
|
menu_bar(self, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
if !self.hide_tab_bar {
|
if self.show_tab_bar && !self.focus_mode {
|
||||||
tab_bar(self, ctx);
|
tab_bar(self, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.show_file_tree && !self.focus_mode {
|
||||||
|
file_tree(self, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.show_bottom_bar && !self.focus_mode {
|
||||||
|
bottom_bar(self, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.show_terminal && !self.focus_mode {
|
||||||
|
shell_bar(self, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
central_panel(self, ctx);
|
central_panel(self, ctx);
|
||||||
|
|
||||||
if self.show_about {
|
if self.show_about && !self.focus_mode {
|
||||||
about_window(self, ctx);
|
about_window(self, ctx);
|
||||||
}
|
}
|
||||||
if self.show_shortcuts {
|
if self.show_shortcuts && !self.focus_mode {
|
||||||
shortcuts_window(self, ctx);
|
shortcuts_window(self, ctx);
|
||||||
}
|
}
|
||||||
if self.show_preferences {
|
if self.show_preferences && !self.focus_mode {
|
||||||
preferences_window(self, ctx);
|
preferences_window(self, ctx);
|
||||||
}
|
}
|
||||||
if self.show_find {
|
if self.show_find {
|
||||||
@ -46,6 +66,9 @@ impl eframe::App for TextEditor {
|
|||||||
self.show_unsaved_changes_dialog(ctx);
|
self.show_unsaved_changes_dialog(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply focus requests at the end of the frame
|
||||||
|
self.focus_manager.apply_focus(ctx);
|
||||||
|
|
||||||
self.prev_show_find = self.show_find;
|
self.prev_show_find = self.show_find;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,12 +11,22 @@ impl TextEditor {
|
|||||||
show_line_numbers: config.show_line_numbers,
|
show_line_numbers: config.show_line_numbers,
|
||||||
word_wrap: config.word_wrap,
|
word_wrap: config.word_wrap,
|
||||||
auto_hide_toolbar: config.auto_hide_toolbar,
|
auto_hide_toolbar: config.auto_hide_toolbar,
|
||||||
hide_tab_bar: config.hide_tab_bar,
|
show_tab_bar: config.show_tab_bar,
|
||||||
|
show_bottom_bar: config.show_bottom_bar,
|
||||||
|
show_file_tree: config.show_file_tree,
|
||||||
|
show_terminal: config.show_terminal,
|
||||||
theme: config.theme,
|
theme: config.theme,
|
||||||
line_side: config.line_side,
|
line_side: config.line_side,
|
||||||
|
file_tree_side: config.file_tree_side,
|
||||||
|
show_hidden_files: config.show_hidden_files,
|
||||||
|
follow_git: config.follow_git,
|
||||||
|
tab_char: config.tab_char,
|
||||||
|
tab_width: config.tab_width,
|
||||||
font_family: config.font_family,
|
font_family: config.font_family,
|
||||||
font_size: config.font_size,
|
font_size: config.font_size,
|
||||||
syntax_highlighting: config.syntax_highlighting,
|
syntax_highlighting: config.syntax_highlighting,
|
||||||
|
auto_indent: config.auto_indent,
|
||||||
|
focus_mode: config.focus_mode,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,6 +90,8 @@ impl TextEditor {
|
|||||||
|
|
||||||
editor.apply_font_settings(&cc.egui_ctx);
|
editor.apply_font_settings(&cc.egui_ctx);
|
||||||
|
|
||||||
|
editor.previous_content = editor.get_active_tab().map(|tab| tab.content.to_owned()).unwrap_or_default();
|
||||||
|
editor.previous_cursor_char_index = Some(0);
|
||||||
editor
|
editor
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,13 +100,23 @@ impl TextEditor {
|
|||||||
state_cache: self.state_cache,
|
state_cache: self.state_cache,
|
||||||
auto_hide_toolbar: self.auto_hide_toolbar,
|
auto_hide_toolbar: self.auto_hide_toolbar,
|
||||||
show_line_numbers: self.show_line_numbers,
|
show_line_numbers: self.show_line_numbers,
|
||||||
hide_tab_bar: self.hide_tab_bar,
|
show_tab_bar: self.show_tab_bar,
|
||||||
|
show_bottom_bar: self.show_bottom_bar,
|
||||||
|
show_file_tree: self.show_file_tree,
|
||||||
|
show_terminal: self.show_terminal,
|
||||||
word_wrap: self.word_wrap,
|
word_wrap: self.word_wrap,
|
||||||
theme: self.theme,
|
theme: self.theme,
|
||||||
line_side: self.line_side,
|
line_side: self.line_side,
|
||||||
|
file_tree_side: self.file_tree_side,
|
||||||
|
show_hidden_files: self.show_hidden_files,
|
||||||
|
follow_git: self.follow_git,
|
||||||
|
tab_char: self.tab_char,
|
||||||
|
tab_width: self.tab_width,
|
||||||
font_family: self.font_family.to_string(),
|
font_family: self.font_family.to_string(),
|
||||||
font_size: self.font_size,
|
font_size: self.font_size,
|
||||||
syntax_highlighting: self.syntax_highlighting,
|
syntax_highlighting: self.syntax_highlighting,
|
||||||
|
auto_indent: self.auto_indent,
|
||||||
|
focus_mode: self.focus_mode,
|
||||||
// vim_mode: self.vim_mode,
|
// vim_mode: self.vim_mode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
use super::editor::TextEditor;
|
use super::editor::TextEditor;
|
||||||
use super::editor::TextProcessingResult;
|
use super::editor::TextProcessingResult;
|
||||||
use crate::app::{tab::Tab, theme::Theme};
|
use crate::app::{tab::Tab, theme::Theme};
|
||||||
|
use egui_commonmark::CommonMarkCache;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
impl Default for TextEditor {
|
impl Default for TextEditor {
|
||||||
@ -14,19 +15,32 @@ 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,
|
||||||
show_line_numbers: false,
|
show_line_numbers: false,
|
||||||
word_wrap: true,
|
word_wrap: true,
|
||||||
auto_hide_toolbar: false,
|
auto_hide_toolbar: false,
|
||||||
hide_tab_bar: true,
|
show_tab_bar: false,
|
||||||
|
show_bottom_bar: true,
|
||||||
|
show_file_tree: false,
|
||||||
|
show_terminal: false,
|
||||||
|
file_tree_root: None,
|
||||||
|
file_tree_state: crate::ui::file_tree::FileTreeState::default(),
|
||||||
syntax_highlighting: false,
|
syntax_highlighting: false,
|
||||||
|
auto_indent: true,
|
||||||
theme: Theme::default(),
|
theme: Theme::default(),
|
||||||
line_side: false,
|
line_side: false,
|
||||||
|
file_tree_side: false,
|
||||||
|
show_hidden_files: false,
|
||||||
|
follow_git: true,
|
||||||
|
tab_char: false,
|
||||||
|
tab_width: 4,
|
||||||
font_family: "Proportional".to_string(),
|
font_family: "Proportional".to_string(),
|
||||||
font_size: 14.0,
|
font_size: 14.0,
|
||||||
font_size_input: None,
|
font_size_input: None,
|
||||||
|
tab_width_input: None,
|
||||||
zoom_factor: 1.0,
|
zoom_factor: 1.0,
|
||||||
menu_interaction_active: false,
|
menu_interaction_active: false,
|
||||||
tab_bar_rect: None,
|
tab_bar_rect: None,
|
||||||
@ -45,10 +59,15 @@ 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,
|
||||||
should_select_current_match: false,
|
should_select_current_match: false,
|
||||||
|
markdown_cache: CommonMarkCache::default(),
|
||||||
|
focus_mode: false,
|
||||||
|
focus_manager: crate::ui::focus_manager::FocusManager::default(),
|
||||||
|
shell_state: crate::ui::shell_bar::ShellState::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,13 @@
|
|||||||
|
use crate::app::actions::ShortcutAction;
|
||||||
use crate::app::tab::Tab;
|
use crate::app::tab::Tab;
|
||||||
use crate::app::theme::Theme;
|
use crate::app::theme::Theme;
|
||||||
|
use crate::io;
|
||||||
|
use crate::ui::file_tree::FileTreeState;
|
||||||
|
use crate::ui::focus_manager::{FocusManager, FocusTarget, priorities};
|
||||||
|
use crate::ui::shell_bar::ShellState;
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
|
use egui_commonmark::CommonMarkCache;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
@ -39,19 +46,32 @@ 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,
|
||||||
pub(crate) show_line_numbers: bool,
|
pub(crate) show_line_numbers: bool,
|
||||||
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) show_tab_bar: bool,
|
||||||
|
pub(crate) show_bottom_bar: bool,
|
||||||
|
pub(crate) show_file_tree: bool,
|
||||||
|
pub(crate) show_terminal: bool,
|
||||||
|
pub(crate) file_tree_root: Option<PathBuf>,
|
||||||
|
pub(crate) file_tree_state: FileTreeState,
|
||||||
pub(crate) syntax_highlighting: bool,
|
pub(crate) syntax_highlighting: bool,
|
||||||
|
pub(crate) auto_indent: bool,
|
||||||
pub(crate) theme: Theme,
|
pub(crate) theme: Theme,
|
||||||
pub(crate) line_side: bool,
|
pub(crate) line_side: bool,
|
||||||
|
pub(crate) file_tree_side: bool,
|
||||||
|
pub(crate) show_hidden_files: bool,
|
||||||
|
pub(crate) follow_git: bool,
|
||||||
|
pub(crate) tab_char: bool,
|
||||||
|
pub(crate) tab_width: usize,
|
||||||
pub(crate) font_family: String,
|
pub(crate) font_family: String,
|
||||||
pub(crate) font_size: f32,
|
pub(crate) font_size: f32,
|
||||||
pub(crate) font_size_input: Option<String>,
|
pub(crate) font_size_input: Option<String>,
|
||||||
|
pub(crate) tab_width_input: Option<String>,
|
||||||
pub(crate) zoom_factor: f32,
|
pub(crate) zoom_factor: f32,
|
||||||
pub(crate) menu_interaction_active: bool,
|
pub(crate) menu_interaction_active: bool,
|
||||||
pub(crate) tab_bar_rect: Option<egui::Rect>,
|
pub(crate) tab_bar_rect: Option<egui::Rect>,
|
||||||
@ -69,9 +89,179 @@ 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,
|
||||||
pub(crate) should_select_current_match: bool,
|
pub(crate) should_select_current_match: bool,
|
||||||
pub(crate) previous_cursor_position: Option<usize>,
|
pub(crate) previous_cursor_position: Option<usize>,
|
||||||
|
pub(crate) markdown_cache: CommonMarkCache,
|
||||||
|
pub(crate) focus_mode: bool,
|
||||||
|
pub(crate) focus_manager: FocusManager,
|
||||||
|
pub(crate) shell_state: ShellState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextEditor {
|
||||||
|
pub fn perform_action(&mut self, action: ShortcutAction) -> bool {
|
||||||
|
match action {
|
||||||
|
ShortcutAction::NewFile => {
|
||||||
|
io::new_file(self);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
ShortcutAction::OpenFile => {
|
||||||
|
io::open_file(self);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
ShortcutAction::SaveFile => {
|
||||||
|
io::save_file(self);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
ShortcutAction::SaveAsFile => {
|
||||||
|
io::save_as_file(self);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
ShortcutAction::NewTab => {
|
||||||
|
self.add_new_tab();
|
||||||
|
false
|
||||||
|
}
|
||||||
|
ShortcutAction::CloseTab => {
|
||||||
|
if self.tabs.len() > 1 {
|
||||||
|
if let Some(current_tab) = self.get_active_tab() {
|
||||||
|
if current_tab.is_modified {
|
||||||
|
self.pending_unsaved_action =
|
||||||
|
Some(UnsavedAction::CloseTab(self.active_tab_index));
|
||||||
|
} else {
|
||||||
|
self.close_tab(self.active_tab_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
ShortcutAction::ToggleLineNumbers => {
|
||||||
|
self.show_line_numbers = !self.show_line_numbers;
|
||||||
|
self.save_config();
|
||||||
|
false
|
||||||
|
}
|
||||||
|
ShortcutAction::ToggleLineSide => {
|
||||||
|
self.line_side = !self.line_side;
|
||||||
|
self.save_config();
|
||||||
|
false
|
||||||
|
}
|
||||||
|
ShortcutAction::ToggleWordWrap => {
|
||||||
|
self.word_wrap = !self.word_wrap;
|
||||||
|
self.save_config();
|
||||||
|
false
|
||||||
|
}
|
||||||
|
ShortcutAction::ToggleAutoHideToolbar => {
|
||||||
|
self.auto_hide_toolbar = !self.auto_hide_toolbar;
|
||||||
|
self.save_config();
|
||||||
|
false
|
||||||
|
}
|
||||||
|
ShortcutAction::ToggleBottomBar => {
|
||||||
|
self.show_bottom_bar = !self.show_bottom_bar;
|
||||||
|
self.save_config();
|
||||||
|
false
|
||||||
|
}
|
||||||
|
ShortcutAction::ToggleFileTree => {
|
||||||
|
self.show_file_tree = !self.show_file_tree;
|
||||||
|
self.save_config();
|
||||||
|
false
|
||||||
|
}
|
||||||
|
ShortcutAction::ToggleFileTreeSide => {
|
||||||
|
self.file_tree_side = !self.file_tree_side;
|
||||||
|
self.save_config();
|
||||||
|
false
|
||||||
|
}
|
||||||
|
ShortcutAction::NextTab => {
|
||||||
|
let next_tab_index = self.active_tab_index + 1;
|
||||||
|
if next_tab_index < self.tabs.len() {
|
||||||
|
self.switch_to_tab(next_tab_index);
|
||||||
|
} else {
|
||||||
|
self.switch_to_tab(0);
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
ShortcutAction::PrevTab => {
|
||||||
|
if self.active_tab_index == 0 {
|
||||||
|
self.switch_to_tab(self.tabs.len() - 1);
|
||||||
|
} else {
|
||||||
|
self.switch_to_tab(self.active_tab_index - 1);
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
ShortcutAction::PageUp | ShortcutAction::PageDown => false,
|
||||||
|
ShortcutAction::ZoomIn => {
|
||||||
|
self.font_size += 1.0;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
ShortcutAction::ZoomOut => {
|
||||||
|
self.font_size -= 1.0;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
ShortcutAction::GlobalZoomIn => {
|
||||||
|
self.zoom_factor += 0.1;
|
||||||
|
false
|
||||||
|
}
|
||||||
|
ShortcutAction::GlobalZoomOut => {
|
||||||
|
self.zoom_factor -= 0.1;
|
||||||
|
if self.zoom_factor < 0.1 {
|
||||||
|
self.zoom_factor = 0.1;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
ShortcutAction::ResetZoom => {
|
||||||
|
self.zoom_factor = 1.0;
|
||||||
|
false
|
||||||
|
}
|
||||||
|
ShortcutAction::ToggleVimMode => {
|
||||||
|
// self.vim_mode = !self.vim_mode;
|
||||||
|
false
|
||||||
|
}
|
||||||
|
ShortcutAction::Escape => {
|
||||||
|
self.show_about = false;
|
||||||
|
self.show_shortcuts = false;
|
||||||
|
if self.show_find {
|
||||||
|
self.should_select_current_match = true;
|
||||||
|
}
|
||||||
|
self.show_find = false;
|
||||||
|
self.show_preferences = false;
|
||||||
|
self.pending_unsaved_action = None;
|
||||||
|
false
|
||||||
|
}
|
||||||
|
ShortcutAction::ToggleFind => {
|
||||||
|
self.show_find = !self.show_find;
|
||||||
|
if self.show_find && !self.find_query.is_empty() {
|
||||||
|
self.update_find_matches();
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
ShortcutAction::ToggleReplace => {
|
||||||
|
self.show_find = !self.show_find;
|
||||||
|
self.show_replace_section = true;
|
||||||
|
if self.show_find && !self.find_query.is_empty() {
|
||||||
|
self.update_find_matches();
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
ShortcutAction::FocusFind => {
|
||||||
|
if self.show_find {
|
||||||
|
self.focus_find = true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
ShortcutAction::Preferences => {
|
||||||
|
self.show_preferences = !self.show_preferences;
|
||||||
|
false
|
||||||
|
}
|
||||||
|
ShortcutAction::ToggleMarkdown => {
|
||||||
|
self.show_markdown = !self.show_markdown;
|
||||||
|
false
|
||||||
|
}
|
||||||
|
ShortcutAction::ToggleFocusMode => {
|
||||||
|
self.focus_mode = !self.focus_mode;
|
||||||
|
self.save_config();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,7 +216,14 @@ 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() {
|
||||||
|
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
|
||||||
@ -217,3 +234,4 @@ impl TextEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
@ -121,25 +103,6 @@ impl TextEditor {
|
|||||||
self.previous_cursor_line = self.current_cursor_line;
|
self.previous_cursor_line = self.current_cursor_line;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_cursor_line_change(
|
|
||||||
&self,
|
|
||||||
old_content: &str,
|
|
||||||
new_content: &str,
|
|
||||||
old_cursor_pos: usize,
|
|
||||||
new_cursor_pos: usize,
|
|
||||||
) -> isize {
|
|
||||||
let old_newlines = Self::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)
|
|
||||||
.bytes()
|
|
||||||
.filter(|&b| b == b'\n')
|
|
||||||
.count();
|
|
||||||
|
|
||||||
new_newlines as isize - old_newlines as isize
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_character_replacement(
|
fn handle_character_replacement(
|
||||||
&mut self,
|
&mut self,
|
||||||
@ -168,41 +131,43 @@ impl TextEditor {
|
|||||||
new_cursor_pos: usize,
|
new_cursor_pos: usize,
|
||||||
ui: &egui::Ui,
|
ui: &egui::Ui,
|
||||||
) {
|
) {
|
||||||
let min_len = old_content.len().min(new_content.len());
|
let old_char_count = old_content.chars().count();
|
||||||
let mut common_prefix = 0;
|
let new_char_count = new_content.chars().count();
|
||||||
let mut common_suffix = 0;
|
|
||||||
for i in 0..min_len {
|
|
||||||
if old_content.as_bytes()[i] == new_content.as_bytes()[i] {
|
|
||||||
common_prefix += 1;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in 0..min_len - common_prefix {
|
let safe_new_cursor = new_cursor_pos.min(new_char_count);
|
||||||
let old_idx = old_content.len() - 1 - i;
|
|
||||||
let new_idx = new_content.len() - 1 - i;
|
|
||||||
if old_content.as_bytes()[old_idx] == new_content.as_bytes()[new_idx] {
|
|
||||||
common_suffix += 1;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let added_start = common_prefix;
|
let new_byte_pos = new_content.char_indices()
|
||||||
let added_end = new_content.len() - common_suffix;
|
.nth(safe_new_cursor)
|
||||||
let added_text = &new_content[added_start..added_end];
|
.map(|(idx, _)| idx)
|
||||||
|
.unwrap_or(new_content.len());
|
||||||
|
|
||||||
|
let added_chars = new_char_count.saturating_sub(old_char_count);
|
||||||
|
let addition_start_byte = new_byte_pos.saturating_sub(
|
||||||
|
new_content[..new_byte_pos]
|
||||||
|
.chars()
|
||||||
|
.rev()
|
||||||
|
.take(added_chars)
|
||||||
|
.map(|c| c.len_utf8())
|
||||||
|
.sum::<usize>()
|
||||||
|
);
|
||||||
|
let addition_end_byte = new_byte_pos;
|
||||||
|
|
||||||
|
let added_text = if addition_start_byte < addition_end_byte && addition_end_byte <= new_content.len() {
|
||||||
|
&new_content[addition_start_byte..addition_end_byte]
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
let newlines_added = added_text.bytes().filter(|&b| b == b'\n').count();
|
let newlines_added = added_text.bytes().filter(|&b| b == b'\n').count();
|
||||||
|
|
||||||
if newlines_added > 0 {
|
if newlines_added > 0 {
|
||||||
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 = new_content[..addition_start_byte]
|
||||||
.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 = new_content[..addition_end_byte]
|
||||||
.bytes()
|
.bytes()
|
||||||
.filter(|&b| b == b'\n')
|
.filter(|&b| b == b'\n')
|
||||||
.count();
|
.count();
|
||||||
@ -218,7 +183,7 @@ impl TextEditor {
|
|||||||
self.update_processing_result(current_result);
|
self.update_processing_result(current_result);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let current_line = self.extract_current_line(new_content, new_cursor_pos);
|
let current_line = self.extract_current_line(new_content, safe_new_cursor);
|
||||||
let current_line_length = current_line.chars().count();
|
let current_line_length = current_line.chars().count();
|
||||||
self.update_line_if_longer(
|
self.update_line_if_longer(
|
||||||
self.current_cursor_line,
|
self.current_cursor_line,
|
||||||
@ -268,11 +233,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 +296,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 +311,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
|
||||||
|
|||||||
@ -36,7 +36,7 @@ impl TextEditor {
|
|||||||
self.update_find_matches();
|
self.update_find_matches();
|
||||||
}
|
}
|
||||||
self.text_needs_processing = true;
|
self.text_needs_processing = true;
|
||||||
|
self.file_tree_state.set_selected(Some(self.get_active_tab().unwrap().file_path.clone().unwrap_or(std::path::PathBuf::new())));
|
||||||
if let Err(e) = self.save_state_cache() {
|
if let Err(e) = self.save_state_cache() {
|
||||||
eprintln!("Failed to save state cache: {e}");
|
eprintln!("Failed to save state cache: {e}");
|
||||||
}
|
}
|
||||||
@ -49,7 +49,10 @@ impl TextEditor {
|
|||||||
if self.show_find && !self.find_query.is_empty() {
|
if self.show_find && !self.find_query.is_empty() {
|
||||||
self.update_find_matches();
|
self.update_find_matches();
|
||||||
}
|
}
|
||||||
|
self.previous_content = self.get_active_tab().unwrap().content.to_owned();
|
||||||
|
self.previous_cursor_char_index = Some(0);
|
||||||
self.text_needs_processing = true;
|
self.text_needs_processing = true;
|
||||||
|
self.file_tree_state.set_selected(Some(self.get_active_tab().unwrap().file_path.clone().unwrap_or(std::path::PathBuf::new())));
|
||||||
|
|
||||||
if let Err(e) = self.save_state_cache() {
|
if let Err(e) = self.save_state_cache() {
|
||||||
eprintln!("Failed to save state cache: {e}");
|
eprintln!("Failed to save state cache: {e}");
|
||||||
|
|||||||
@ -46,6 +46,7 @@ impl TextEditor {
|
|||||||
egui::TextStyle::Monospace,
|
egui::TextStyle::Monospace,
|
||||||
egui::FontId::new(self.font_size, font_family),
|
egui::FontId::new(self.font_size, font_family),
|
||||||
);
|
);
|
||||||
|
self.font_size_input = Some(self.font_size.to_string());
|
||||||
|
|
||||||
ctx.set_style(style);
|
ctx.set_style(style);
|
||||||
self.font_settings_changed = true;
|
self.font_settings_changed = true;
|
||||||
@ -65,18 +66,18 @@ impl TextEditor {
|
|||||||
let processing_result = self.get_text_processing_result();
|
let processing_result = self.get_text_processing_result();
|
||||||
let line_count = processing_result.line_count;
|
let line_count = processing_result.line_count;
|
||||||
|
|
||||||
let font_id = self.get_font_id();
|
let monospace_font_id = egui::FontId::monospace(self.font_size);
|
||||||
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_no_wrap(sample_text, monospace_font_id, egui::Color32::WHITE)
|
||||||
.size()
|
.size()
|
||||||
.x
|
.x
|
||||||
});
|
});
|
||||||
|
|
||||||
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 +96,149 @@ 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 char_count = content.chars().count();
|
||||||
|
let safe_char_pos = self.current_cursor_index.min(char_count);
|
||||||
|
|
||||||
|
// Convert character index to byte index
|
||||||
|
let byte_pos = content
|
||||||
|
.char_indices()
|
||||||
|
.nth(safe_char_pos)
|
||||||
|
.map(|(byte_idx, _)| byte_idx)
|
||||||
|
.unwrap_or(content.len());
|
||||||
|
|
||||||
|
// Calculate column (chars since last newline)
|
||||||
|
let mut column = 0;
|
||||||
|
for c in content[..byte_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 char_count = content.chars().count();
|
||||||
|
let safe_char_pos = current_pos.min(char_count);
|
||||||
|
|
||||||
|
// Convert character index to byte index
|
||||||
|
let byte_pos = content
|
||||||
|
.char_indices()
|
||||||
|
.nth(safe_char_pos)
|
||||||
|
.map(|(byte_idx, _)| byte_idx)
|
||||||
|
.unwrap_or(content.len());
|
||||||
|
|
||||||
|
let mut result_byte_pos = byte_pos;
|
||||||
|
let mut lines_moved = 0;
|
||||||
|
|
||||||
|
// char_indices() returns (byte_index, char), so idx is a byte index
|
||||||
|
for (idx, ch) in content[byte_pos..].char_indices() {
|
||||||
|
if ch == '\n' {
|
||||||
|
lines_moved += 1;
|
||||||
|
if lines_moved >= lines {
|
||||||
|
result_byte_pos = byte_pos + idx + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if lines_moved < lines && result_byte_pos == byte_pos {
|
||||||
|
result_byte_pos = content.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert byte index back to character index
|
||||||
|
content[..result_byte_pos.min(content.len())].chars().count()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_cursor_up_lines(content: &str, current_pos: usize, lines: usize) -> usize {
|
||||||
|
let char_count = content.chars().count();
|
||||||
|
let safe_char_pos = current_pos.min(char_count);
|
||||||
|
|
||||||
|
// Convert character index to byte index
|
||||||
|
let byte_pos = content
|
||||||
|
.char_indices()
|
||||||
|
.nth(safe_char_pos)
|
||||||
|
.map(|(byte_idx, _)| byte_idx)
|
||||||
|
.unwrap_or(content.len());
|
||||||
|
|
||||||
|
let mut result_byte_pos = byte_pos;
|
||||||
|
let mut lines_moved = 0;
|
||||||
|
|
||||||
|
// Use char_indices() and iterate in reverse to get correct byte positions
|
||||||
|
for (byte_idx, ch) in content[..byte_pos].char_indices().rev() {
|
||||||
|
if ch == '\n' {
|
||||||
|
lines_moved += 1;
|
||||||
|
if lines_moved >= lines {
|
||||||
|
result_byte_pos = byte_idx + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result_byte_pos = byte_idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert byte index back to character index
|
||||||
|
content[..result_byte_pos].chars().count()
|
||||||
|
}
|
||||||
|
|||||||
46
src/io.rs
46
src/io.rs
@ -250,3 +250,49 @@ pub(crate) fn save_to_path(app: &mut TextEditor, path: PathBuf) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn rename_file(app: &mut TextEditor, old_path: &PathBuf, new_name: &str) -> Result<(), String> {
|
||||||
|
let parent = old_path.parent().ok_or("Cannot rename root directory")?;
|
||||||
|
let new_path = parent.join(new_name);
|
||||||
|
|
||||||
|
// If renaming to the same path, just return success (no-op)
|
||||||
|
if new_path == *old_path {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if target already exists
|
||||||
|
if new_path.exists() {
|
||||||
|
return Err(format!("File {} already exists", new_path.display()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename the file on disk
|
||||||
|
fs::rename(old_path, &new_path)
|
||||||
|
.map_err(|e| format!("Failed to rename file: {}", e))?;
|
||||||
|
|
||||||
|
// Update any tabs that reference this file
|
||||||
|
for tab in &mut app.tabs {
|
||||||
|
if let Some(tab_path) = &tab.file_path {
|
||||||
|
// Check if this tab's path matches the old path
|
||||||
|
if tab_path == old_path {
|
||||||
|
tab.file_path = Some(new_path.clone());
|
||||||
|
tab.title = new_path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or("Untitled")
|
||||||
|
.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the active tab index if needed
|
||||||
|
if let Some(active_tab) = app.get_active_tab() {
|
||||||
|
if let Some(tab_path) = &active_tab.file_path {
|
||||||
|
if tab_path == &new_path {
|
||||||
|
// The active tab was updated, mark as needing processing
|
||||||
|
app.text_needs_processing = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@ -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,8 +1,12 @@
|
|||||||
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;
|
||||||
|
pub(crate) mod file_tree;
|
||||||
|
pub(crate) mod focus_manager;
|
||||||
pub(crate) mod menu_bar;
|
pub(crate) mod menu_bar;
|
||||||
pub(crate) mod preferences_window;
|
pub(crate) mod preferences_window;
|
||||||
pub(crate) mod shortcuts_window;
|
pub(crate) mod shortcuts_window;
|
||||||
pub(crate) mod tab_bar;
|
pub(crate) mod tab_bar;
|
||||||
|
pub(crate) mod shell_bar;
|
||||||
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.show_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,22 +1,35 @@
|
|||||||
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::*;
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
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;
|
||||||
let word_wrap = app.word_wrap;
|
let word_wrap = app.word_wrap;
|
||||||
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 monospace = if app.font_family.as_str() == "Monospace" { true } else { false };
|
||||||
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 focus_mode = app.focus_mode;
|
||||||
|
|
||||||
let _output = egui::CentralPanel::default()
|
let _output = egui::CentralPanel::default()
|
||||||
.frame(egui::Frame::NONE)
|
.frame(egui::Frame::NONE)
|
||||||
@ -26,55 +39,81 @@ 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();
|
||||||
|
|
||||||
if !show_line_numbers || app.get_active_tab().is_none() {
|
// 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() || focus_mode {
|
||||||
let _scroll_response =
|
let _scroll_response =
|
||||||
egui::ScrollArea::vertical()
|
egui::ScrollArea::vertical()
|
||||||
.auto_shrink([false; 2])
|
.auto_shrink([false; 2])
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
let full_rect = ui.available_rect_before_wrap();
|
let full_rect = ui.available_rect_before_wrap();
|
||||||
let context_response =
|
|
||||||
ui.allocate_response(full_rect.size(), egui::Sense::click());
|
|
||||||
|
|
||||||
ui.scope_builder(egui::UiBuilder::new().max_rect(full_rect), |ui| {
|
ui.scope_builder(egui::UiBuilder::new().max_rect(full_rect), |ui| {
|
||||||
editor_view_ui(ui, app);
|
editor_view_ui(ui, app);
|
||||||
});
|
});
|
||||||
|
|
||||||
handle_empty(ui, app, &context_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,29 +127,51 @@ 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,
|
||||||
|
monospace,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
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();
|
||||||
let context_response = ui.allocate_response(
|
|
||||||
full_rect.size(),
|
|
||||||
egui::Sense::click(),
|
|
||||||
);
|
|
||||||
|
|
||||||
ui.scope_builder(
|
ui.scope_builder(
|
||||||
egui::UiBuilder::new().max_rect(full_rect),
|
egui::UiBuilder::new().max_rect(full_rect),
|
||||||
|ui| {
|
|ui| editor_view_ui(ui, app),
|
||||||
editor_view_ui(ui, app);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
handle_empty(ui, app, &context_response);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
separator_widget(ui);
|
separator_widget(ui);
|
||||||
@ -119,24 +180,17 @@ 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);
|
||||||
separator_widget(ui);
|
separator_widget(ui);
|
||||||
|
|
||||||
let editor_area = ui.available_rect_before_wrap();
|
let editor_area = ui.available_rect_before_wrap();
|
||||||
let context_response =
|
|
||||||
ui.allocate_response(editor_area.size(), egui::Sense::click());
|
|
||||||
|
|
||||||
ui.scope_builder(
|
ui.scope_builder(
|
||||||
egui::UiBuilder::new().max_rect(editor_area),
|
egui::UiBuilder::new().max_rect(editor_area),
|
||||||
|ui| {
|
|ui| editor_view_ui(ui, app),
|
||||||
editor_view_ui(ui, app);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
handle_empty(ui, app, &context_response);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -144,74 +198,3 @@ 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) {
|
|
||||||
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) {
|
|
||||||
if let Some(active_tab) = app.get_active_tab() {
|
|
||||||
let text_len = active_tab.content.len();
|
|
||||||
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);
|
|
||||||
|
|
||||||
_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();
|
|
||||||
let reset_zoom_key = egui::Id::new("editor_reset_zoom");
|
|
||||||
|
|
||||||
if ui.button("Cut").clicked() {
|
|
||||||
ui.ctx()
|
|
||||||
.send_viewport_cmd(egui::ViewportCommand::RequestCut);
|
|
||||||
ui.close_kind(UiKind::Menu);
|
|
||||||
}
|
|
||||||
if ui.button("Copy").clicked() {
|
|
||||||
ui.ctx()
|
|
||||||
.send_viewport_cmd(egui::ViewportCommand::RequestCopy);
|
|
||||||
ui.close_kind(UiKind::Menu);
|
|
||||||
}
|
|
||||||
if ui.button("Paste").clicked() {
|
|
||||||
ui.ctx()
|
|
||||||
.send_viewport_cmd(egui::ViewportCommand::RequestPaste);
|
|
||||||
ui.close_kind(UiKind::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_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) {
|
|
||||||
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("Reset Zoom").clicked() {
|
|
||||||
ui.ctx().memory_mut(|mem| {
|
|
||||||
mem.data.insert_temp(reset_zoom_key, true);
|
|
||||||
});
|
|
||||||
ui.close_kind(UiKind::Menu);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,37 +1,36 @@
|
|||||||
use crate::app::TextEditor;
|
use crate::app::TextEditor;
|
||||||
|
use crate::ui::focus_manager::{FocusTarget, priorities};
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use egui_extras::syntax_highlighting::{self};
|
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 show_terminal = app.show_terminal;
|
||||||
|
let is_renaming = app
|
||||||
|
.get_active_tab()
|
||||||
|
.and_then(|tab| tab.file_path.as_ref())
|
||||||
|
.map(|file_path| app.file_tree_state.is_renaming(file_path))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
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 auto_indent = app.auto_indent;
|
||||||
|
let tab_char = app.tab_char;
|
||||||
|
let tab_width = app.tab_width;
|
||||||
|
|
||||||
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();
|
||||||
ui.painter().rect_filled(editor_rect, 0.0, bg_color);
|
ui.painter().rect_filled(editor_rect, 0.0, bg_color);
|
||||||
|
|
||||||
let reset_zoom_key = egui::Id::new("editor_reset_zoom");
|
handle_zoom_reset(ui, app);
|
||||||
let should_reset_zoom = ui
|
|
||||||
.ctx()
|
|
||||||
.memory_mut(|mem| mem.data.get_temp::<bool>(reset_zoom_key).unwrap_or(false));
|
|
||||||
|
|
||||||
if should_reset_zoom {
|
|
||||||
app.zoom_factor = 1.0;
|
|
||||||
ui.ctx().set_zoom_factor(1.0);
|
|
||||||
ui.ctx().memory_mut(|mem| {
|
|
||||||
mem.data.insert_temp(reset_zoom_key, false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let (estimated_width, desired_width) = if !word_wrap {
|
let (estimated_width, desired_width) = if !word_wrap {
|
||||||
(app.calculate_content_based_width(ui), f32::INFINITY)
|
(app.calculate_content_based_width(ui), f32::INFINITY)
|
||||||
@ -55,56 +54,49 @@ 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| {
|
draw_editor_highlights(ui, &find_data, &font_id, font_size, desired_width);
|
||||||
fonts.layout(
|
};
|
||||||
content.to_owned(),
|
|
||||||
font_id.to_owned(),
|
|
||||||
ui.visuals().text_color(),
|
|
||||||
desired_width,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let text_area_left = editor_rect.left() + 4.0; // Text Editor default margins
|
|
||||||
let text_area_top = editor_rect.top() + 2.0;
|
|
||||||
|
|
||||||
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 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| {
|
||||||
// let syntect_theme =
|
let layout_job = editor_layouter(
|
||||||
// crate::app::theme::create_code_theme_from_visuals(ui.visuals(), font_size);
|
ui,
|
||||||
let theme = egui_extras::syntax_highlighting::CodeTheme::from_style(ui.style());
|
string,
|
||||||
let text = string.as_str();
|
wrap_width,
|
||||||
let mut layout_job = if syntax_highlighting_enabled && language != "txt" {
|
syntax_highlighting_enabled,
|
||||||
// let mut settings = egui_extras::syntax_highlighting::SyntectSettings::default();
|
&language,
|
||||||
// settings.ts = syntect_theme;
|
&font_id,
|
||||||
// syntax_highlighting::highlight_with(ui.ctx(), &ui.style().clone(), &theme, text, &language, &settings)
|
);
|
||||||
syntax_highlighting::highlight(ui.ctx(), &ui.style().clone(), &theme, text, &language)
|
ui.fonts_mut(|f| f.layout_job(layout_job))
|
||||||
} else {
|
|
||||||
syntax_highlighting::highlight(ui.ctx(), &ui.style().clone(), &theme, text, "")
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if syntax_highlighting_enabled && language != "txt" {
|
let id_source = active_tab
|
||||||
for section in &mut layout_job.sections {
|
.file_path
|
||||||
section.format.font_id = font_id.clone();
|
.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 mut indent_result = false;
|
||||||
|
let mut tab_result = false;
|
||||||
|
if should_have_focus(
|
||||||
|
show_find,
|
||||||
|
show_preferences,
|
||||||
|
show_about,
|
||||||
|
show_shortcuts,
|
||||||
|
is_renaming,
|
||||||
|
show_terminal,
|
||||||
|
) {
|
||||||
|
indent_result = handle_auto_indent(ui, active_tab, text_edit_id, auto_indent);
|
||||||
|
tab_result = handle_tab_insertion(ui, active_tab, text_edit_id, tab_char, tab_width);
|
||||||
}
|
}
|
||||||
|
|
||||||
layout_job.wrap.max_width = wrap_width;
|
let allow_interaction = ui.is_enabled()
|
||||||
ui.fonts(|f| f.layout_job(layout_job))
|
&& !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 +105,11 @@ 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 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,59 +118,193 @@ 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, previous_cursor_position);
|
||||||
|
output
|
||||||
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.inner
|
.inner
|
||||||
.inner
|
.inner
|
||||||
};
|
};
|
||||||
|
|
||||||
let content_changed = output.response.changed();
|
ensure_cursor_visible(ui, &output, &font_id, previous_cursor_position);
|
||||||
let content_for_processing = if content_changed {
|
|
||||||
active_tab.update_modified_state();
|
|
||||||
Some(active_tab.content.to_owned())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if content_changed {
|
handle_post_render_updates(ui, app, &output, indent_result, tab_result);
|
||||||
if let Err(e) = app.save_state_cache() {
|
|
||||||
eprintln!("Failed to save state cache: {e}");
|
output.response.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() {
|
||||||
|
ui.ctx()
|
||||||
|
.send_viewport_cmd(egui::ViewportCommand::RequestCut);
|
||||||
|
ui.close_kind(egui::UiKind::Menu);
|
||||||
|
}
|
||||||
|
if ui.button("Copy").clicked() {
|
||||||
|
ui.ctx()
|
||||||
|
.send_viewport_cmd(egui::ViewportCommand::RequestCopy);
|
||||||
|
ui.close_kind(egui::UiKind::Menu);
|
||||||
|
}
|
||||||
|
if ui.button("Paste").clicked() {
|
||||||
|
ui.ctx()
|
||||||
|
.send_viewport_cmd(egui::ViewportCommand::RequestPaste);
|
||||||
|
ui.close_kind(egui::UiKind::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_kind(egui::UiKind::Menu);
|
||||||
|
}
|
||||||
|
if ui.button("Select All").clicked() {
|
||||||
|
let text_edit_id = output.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),
|
||||||
|
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(egui::UiKind::Menu);
|
||||||
|
}
|
||||||
|
ui.separator();
|
||||||
|
if ui.button("Reset Zoom").clicked() {
|
||||||
|
ui.ctx().memory_mut(|mem| {
|
||||||
|
mem.data.insert_temp(reset_zoom_key, true);
|
||||||
|
});
|
||||||
|
ui.close_kind(egui::UiKind::Menu);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if !output.response.has_focus()
|
||||||
|
&& should_have_focus(
|
||||||
|
show_find,
|
||||||
|
show_preferences,
|
||||||
|
show_about,
|
||||||
|
show_shortcuts,
|
||||||
|
is_renaming,
|
||||||
|
show_terminal,
|
||||||
|
)
|
||||||
|
// Don't steal focus during file tree renaming or when terminal is showing
|
||||||
|
{
|
||||||
|
app.focus_manager
|
||||||
|
.request_focus(FocusTarget::Editor, priorities::NORMAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
output.response
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_have_focus(
|
||||||
|
show_find: bool,
|
||||||
|
show_preferences: bool,
|
||||||
|
show_about: bool,
|
||||||
|
show_shortcuts: bool,
|
||||||
|
is_renaming: bool,
|
||||||
|
show_terminal: bool,
|
||||||
|
) -> bool {
|
||||||
|
!show_find
|
||||||
|
&& !show_preferences
|
||||||
|
&& !show_about
|
||||||
|
&& !show_shortcuts
|
||||||
|
&& !is_renaming
|
||||||
|
&& !show_terminal
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_zoom_reset(ui: &mut egui::Ui, app: &mut TextEditor) {
|
||||||
|
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));
|
||||||
|
|
||||||
|
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 content_changed && app.show_find && !app.find_query.is_empty() {
|
fn ensure_cursor_visible(
|
||||||
app.update_find_matches();
|
ui: &mut egui::Ui,
|
||||||
}
|
output: &egui::text_edit::TextEditOutput,
|
||||||
|
font_id: &egui::FontId,
|
||||||
|
previous_cursor_position: Option<usize>,
|
||||||
|
) {
|
||||||
let current_cursor_pos = output
|
let current_cursor_pos = output
|
||||||
.state
|
.state
|
||||||
.cursor
|
.cursor
|
||||||
.char_range()
|
.char_range()
|
||||||
.map(|range| range.primary.index);
|
.map(|range| range.primary.index);
|
||||||
|
|
||||||
if let Some(content) = content_for_processing {
|
if let Some(cursor_pos) = current_cursor_pos {
|
||||||
let previous_content = app.previous_content.to_owned();
|
let cursor_moved = Some(cursor_pos) != previous_cursor_position;
|
||||||
let previous_cursor_pos = app.previous_cursor_char_index;
|
let text_changed = output.response.changed();
|
||||||
|
|
||||||
if !previous_content.is_empty() {
|
// Check if there's an active text selection
|
||||||
if let (Some(prev_cursor_pos), Some(curr_cursor_pos)) =
|
let has_selection = output
|
||||||
(previous_cursor_pos, current_cursor_pos)
|
.state
|
||||||
{
|
.cursor
|
||||||
app.process_incremental_change(
|
.char_range()
|
||||||
&previous_content,
|
.map(|range| range.primary.index != range.secondary.index)
|
||||||
&content,
|
.unwrap_or(false);
|
||||||
prev_cursor_pos,
|
|
||||||
curr_cursor_pos,
|
if cursor_moved || text_changed {
|
||||||
ui,
|
let visible_area = ui.clip_rect();
|
||||||
);
|
|
||||||
|
if has_selection && output.response.dragged() {
|
||||||
|
if let Some(mouse_pos) = ui.input(|i| i.pointer.hover_pos()) {
|
||||||
|
if !visible_area.contains(mouse_pos) {
|
||||||
|
let line_height = ui.fonts_mut(|fonts| fonts.row_height(font_id));
|
||||||
|
let margin = egui::vec2(20.0, line_height); // Smaller margin for mouse-following
|
||||||
|
let target_rect =
|
||||||
|
egui::Rect::from_center_size(mouse_pos, egui::vec2(1.0, 1.0))
|
||||||
|
.expand2(margin);
|
||||||
|
ui.scroll_to_rect(target_rect, Some(egui::Align::Center));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
app.process_text_for_rendering(&content, ui);
|
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);
|
||||||
|
if !visible_area.contains_rect(target_rect) {
|
||||||
|
ui.scroll_to_rect(target_rect, Some(egui::Align::Center));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app.previous_content = content.to_owned();
|
fn handle_post_render_updates(
|
||||||
app.previous_cursor_char_index = current_cursor_pos;
|
ui: &mut egui::Ui,
|
||||||
|
app: &mut TextEditor,
|
||||||
|
output: &egui::text_edit::TextEditOutput,
|
||||||
|
indent_result: bool,
|
||||||
|
tab_result: bool,
|
||||||
|
) {
|
||||||
|
let content_changed = output.response.changed() || indent_result || tab_result;
|
||||||
|
let current_cursor_pos = output
|
||||||
|
.state
|
||||||
|
.cursor
|
||||||
|
.char_range()
|
||||||
|
.map(|range| range.primary.index);
|
||||||
|
|
||||||
|
if content_changed {
|
||||||
|
handle_content_change(app, current_cursor_pos, ui);
|
||||||
}
|
}
|
||||||
|
|
||||||
if app.font_settings_changed || app.text_needs_processing {
|
if app.font_settings_changed || app.text_needs_processing {
|
||||||
@ -189,49 +317,255 @@ 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;
|
app.previous_cursor_position = Some(cursor_pos);
|
||||||
let text_changed = output.response.changed();
|
app.current_cursor_index = cursor_pos;
|
||||||
|
update_cursor_line_info(app, cursor_pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if cursor_moved || text_changed {
|
fn handle_content_change(
|
||||||
|
app: &mut TextEditor,
|
||||||
|
current_cursor_pos: Option<usize>,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
) {
|
||||||
|
let Some(active_tab) = app.get_active_tab_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
active_tab.update_modified_state();
|
||||||
|
let content = active_tab.content.to_owned();
|
||||||
|
|
||||||
|
if let Err(e) = app.save_state_cache() {
|
||||||
|
eprintln!("Failed to save state cache: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.show_find && !app.find_query.is_empty() {
|
||||||
|
app.update_find_matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
let previous_content = app.previous_content.to_owned();
|
||||||
|
let previous_cursor_pos = app.previous_cursor_char_index;
|
||||||
|
|
||||||
|
if !previous_content.is_empty()
|
||||||
|
&& let (Some(prev_cursor_pos), Some(curr_cursor_pos)) =
|
||||||
|
(previous_cursor_pos, current_cursor_pos)
|
||||||
|
{
|
||||||
|
app.process_incremental_change(
|
||||||
|
&previous_content,
|
||||||
|
&content,
|
||||||
|
prev_cursor_pos,
|
||||||
|
curr_cursor_pos,
|
||||||
|
ui,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
app.process_text_for_rendering(&content, ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.previous_content = content;
|
||||||
|
app.previous_cursor_char_index = current_cursor_pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_cursor_line_info(app: &mut TextEditor, cursor_pos: usize) {
|
||||||
if let Some(active_tab) = app.get_active_tab() {
|
if let Some(active_tab) = app.get_active_tab() {
|
||||||
let content = &active_tab.content;
|
let content = &active_tab.content;
|
||||||
let cursor_line = content
|
let char_count = content.chars().count();
|
||||||
|
let safe_char_pos = cursor_pos.min(char_count);
|
||||||
|
|
||||||
|
// Convert character index to byte index
|
||||||
|
let byte_pos = content
|
||||||
.char_indices()
|
.char_indices()
|
||||||
.take_while(|(byte_pos, _)| *byte_pos < cursor_pos)
|
.nth(safe_char_pos)
|
||||||
.filter(|(_, ch)| *ch == '\n')
|
.map(|(byte_idx, _)| byte_idx)
|
||||||
.count();
|
.unwrap_or(content.len());
|
||||||
|
|
||||||
let font_id = ui
|
// Count newlines before cursor for line number
|
||||||
.style()
|
let line_number = content[..byte_pos].chars().filter(|&c| c == '\n').count() + 1;
|
||||||
.text_styles
|
app.current_cursor_line = line_number;
|
||||||
.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);
|
fn draw_editor_highlights(
|
||||||
let cursor_rect = egui::Rect::from_min_size(
|
ui: &mut egui::Ui,
|
||||||
egui::pos2(output.response.rect.left(), y_pos),
|
find_data: &Option<(String, Vec<(usize, usize)>, Option<usize>)>,
|
||||||
egui::vec2(2.0, line_height),
|
font_id: &egui::FontId,
|
||||||
|
font_size: f32,
|
||||||
|
wrap_width: f32,
|
||||||
|
) {
|
||||||
|
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(),
|
||||||
|
wrap_width - 8.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(
|
||||||
|
ui,
|
||||||
|
content,
|
||||||
|
matches,
|
||||||
|
*current_match_index,
|
||||||
|
&temp_galley,
|
||||||
|
text_area_left,
|
||||||
|
text_area_top,
|
||||||
|
font_size,
|
||||||
);
|
);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if !output.response.has_focus()
|
fn editor_layouter(
|
||||||
&& !show_preferences
|
ui: &egui::Ui,
|
||||||
&& !show_about
|
string: &dyn egui::TextBuffer,
|
||||||
&& !show_shortcuts
|
wrap_width: f32,
|
||||||
&& !show_find
|
syntax_highlighting_enabled: bool,
|
||||||
{
|
language: &str,
|
||||||
output.response.request_focus();
|
font_id: &egui::FontId,
|
||||||
|
) -> egui::text::LayoutJob {
|
||||||
|
let theme = egui_extras::syntax_highlighting::CodeTheme::from_style(ui.style());
|
||||||
|
let text = string.as_str();
|
||||||
|
|
||||||
|
let mut layout_job = if syntax_highlighting_enabled && language != "txt" {
|
||||||
|
syntax_highlighting::highlight(ui.ctx(), &ui.style().clone(), &theme, text, language)
|
||||||
|
} else {
|
||||||
|
syntax_highlighting::highlight(ui.ctx(), &ui.style().clone(), &theme, text, "")
|
||||||
|
};
|
||||||
|
|
||||||
|
if syntax_highlighting_enabled && language != "txt" {
|
||||||
|
for section in &mut layout_job.sections {
|
||||||
|
section.format.font_id = font_id.to_owned();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
output.response
|
layout_job.wrap.max_width = wrap_width;
|
||||||
|
layout_job
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_auto_indent(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
active_tab: &mut crate::app::tab::Tab,
|
||||||
|
text_edit_id: egui::Id,
|
||||||
|
auto_indent: bool,
|
||||||
|
) -> bool {
|
||||||
|
if !auto_indent || !ui.input(|i| i.key_pressed(egui::Key::Enter) && i.modifiers.is_none()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
||||||
|
if let Some(cursor_range) = state.cursor.char_range() {
|
||||||
|
let cursor_pos = cursor_range.primary.index;
|
||||||
|
let content = &active_tab.content;
|
||||||
|
|
||||||
|
// Find previous line's indentation
|
||||||
|
let byte_idx = content
|
||||||
|
.char_indices()
|
||||||
|
.nth(cursor_pos)
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.unwrap_or(content.len());
|
||||||
|
let before_cursor = &content[..byte_idx];
|
||||||
|
let line_start = before_cursor.rfind('\n').map(|i| i + 1).unwrap_or(0);
|
||||||
|
let current_line = &before_cursor[line_start..];
|
||||||
|
let indentation: String = current_line
|
||||||
|
.chars()
|
||||||
|
.take_while(|&c| c == ' ' || c == '\t')
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Replace selection (or just insert at cursor) with newline + indentation
|
||||||
|
let start_char = cursor_range.primary.index.min(cursor_range.secondary.index);
|
||||||
|
let end_char = cursor_range.primary.index.max(cursor_range.secondary.index);
|
||||||
|
|
||||||
|
let start_byte = content
|
||||||
|
.char_indices()
|
||||||
|
.nth(start_char)
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.unwrap_or(content.len());
|
||||||
|
let end_byte = content
|
||||||
|
.char_indices()
|
||||||
|
.nth(end_char)
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.unwrap_or(content.len());
|
||||||
|
|
||||||
|
let insert_text = format!("\n{}", indentation);
|
||||||
|
active_tab
|
||||||
|
.content
|
||||||
|
.replace_range(start_byte..end_byte, &insert_text);
|
||||||
|
|
||||||
|
// Update cursor position
|
||||||
|
let new_pos = start_char + insert_text.chars().count();
|
||||||
|
state
|
||||||
|
.cursor
|
||||||
|
.set_char_range(Some(egui::text::CCursorRange::one(
|
||||||
|
egui::text::CCursor::new(new_pos),
|
||||||
|
)));
|
||||||
|
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
||||||
|
|
||||||
|
// Consume the event so TextEdit doesn't process it
|
||||||
|
ui.input_mut(|i| {
|
||||||
|
i.events.retain(|e| {
|
||||||
|
!matches!(
|
||||||
|
e,
|
||||||
|
egui::Event::Key {
|
||||||
|
key: egui::Key::Enter,
|
||||||
|
pressed: true,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_tab_insertion(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
active_tab: &mut crate::app::tab::Tab,
|
||||||
|
text_edit_id: egui::Id,
|
||||||
|
tab_char: bool,
|
||||||
|
tab_width: usize,
|
||||||
|
) -> bool {
|
||||||
|
if tab_char || !ui.input(|i| i.key_pressed(egui::Key::Tab) && i.modifiers.is_none()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
||||||
|
if let Some(cursor_range) = state.cursor.char_range() {
|
||||||
|
let cursor_pos = cursor_range.primary.index;
|
||||||
|
let content = &active_tab.content;
|
||||||
|
let byte_idx = content
|
||||||
|
.char_indices()
|
||||||
|
.nth(cursor_pos)
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.unwrap_or(content.len());
|
||||||
|
let insert_text = " ".repeat(tab_width);
|
||||||
|
active_tab
|
||||||
|
.content
|
||||||
|
.replace_range(byte_idx..byte_idx, &insert_text);
|
||||||
|
let new_pos = cursor_pos + insert_text.chars().count();
|
||||||
|
state
|
||||||
|
.cursor
|
||||||
|
.set_char_range(Some(egui::text::CCursorRange::one(
|
||||||
|
egui::text::CCursor::new(new_pos),
|
||||||
|
)));
|
||||||
|
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
||||||
|
ui.input_mut(|i| {
|
||||||
|
i.events.retain(|e| {
|
||||||
|
!matches!(
|
||||||
|
e,
|
||||||
|
egui::Event::Key {
|
||||||
|
key: egui::Key::Tab,
|
||||||
|
pressed: true,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
// Right side: left-align with trailing space for scrollbar clearance
|
||||||
format!("{:<width$} ", line_number, width = line_count_width)
|
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,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -55,12 +57,23 @@ pub(super) fn render_line_numbers(
|
|||||||
word_wrap: bool,
|
word_wrap: bool,
|
||||||
line_side: bool,
|
line_side: bool,
|
||||||
font_size: f32,
|
font_size: f32,
|
||||||
|
monospace: bool,
|
||||||
) {
|
) {
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
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.add_space(2.0); // Text Editor default top margin
|
let spacing_adjustment = if monospace {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
match font_size {
|
||||||
|
10.0 | 16.0 | 22.0 | 23.0 | 28.0 | 29.0 | 30.0 => -1.0,
|
||||||
|
_ => 0.0,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.spacing_mut().item_spacing.y = spacing_adjustment;
|
||||||
|
ui.add_space(if monospace { 1.0 } else { 2.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;
|
||||||
|
|
||||||
|
pub(super) fn markdown_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) {
|
||||||
|
let content = if let Some(active_tab) = app.get_active_tab() {
|
||||||
|
active_tab.content.clone()
|
||||||
|
} else {
|
||||||
|
ui.label("No file open");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
CommonMarkViewer::new().show(ui, &mut app.markdown_cache, &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;
|
||||||
|
|||||||
932
src/ui/file_tree.rs
Normal file
932
src/ui/file_tree.rs
Normal file
@ -0,0 +1,932 @@
|
|||||||
|
use crate::app::TextEditor;
|
||||||
|
use crate::ui::focus_manager::{FocusTarget, priorities};
|
||||||
|
use egui::Ui;
|
||||||
|
use std::fs;
|
||||||
|
use std::io::BufRead;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct FileTreeState {
|
||||||
|
expanded_paths: std::collections::HashSet<PathBuf>,
|
||||||
|
selected_path: Option<PathBuf>,
|
||||||
|
renaming_path: Option<PathBuf>,
|
||||||
|
rename_text: String,
|
||||||
|
clipboard_paths: Vec<PathBuf>,
|
||||||
|
clipboard_operation: Option<ClipboardOperation>,
|
||||||
|
rename_focus_requested: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum ClipboardOperation {
|
||||||
|
Cut,
|
||||||
|
Copy,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileTreeState {
|
||||||
|
pub fn is_expanded(&self, path: &Path) -> bool {
|
||||||
|
self.expanded_paths.contains(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_expand(&mut self, path: &Path) {
|
||||||
|
if self.expanded_paths.contains(path) {
|
||||||
|
self.expanded_paths.remove(path);
|
||||||
|
} else {
|
||||||
|
self.expanded_paths.insert(path.to_path_buf());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_selected(&self, path: &Path) -> bool {
|
||||||
|
self.selected_path.as_ref().map_or(false, |p| p == path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_selected(&mut self, path: Option<PathBuf>) {
|
||||||
|
self.selected_path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_renaming(&self, path: &Path) -> bool {
|
||||||
|
self.renaming_path.as_ref().map_or(false, |p| p == path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_rename(&mut self, path: &Path, initial_name: &str) {
|
||||||
|
self.renaming_path = Some(path.to_path_buf());
|
||||||
|
self.rename_text = initial_name.to_string();
|
||||||
|
self.rename_focus_requested = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cancel_rename(&mut self) {
|
||||||
|
self.renaming_path = None;
|
||||||
|
self.rename_text.clear();
|
||||||
|
self.rename_focus_requested = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finish_rename(&mut self) -> Option<(PathBuf, String)> {
|
||||||
|
if let Some(old_path) = self.renaming_path.take() {
|
||||||
|
let new_name = self.rename_text.clone();
|
||||||
|
self.rename_text.clear();
|
||||||
|
self.rename_focus_requested = false;
|
||||||
|
Some((old_path, new_name))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_clipboard(&mut self, paths: Vec<PathBuf>, operation: ClipboardOperation) {
|
||||||
|
self.clipboard_paths = paths;
|
||||||
|
self.clipboard_operation = Some(operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_clipboard(&mut self) {
|
||||||
|
self.clipboard_paths.clear();
|
||||||
|
self.clipboard_operation = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_clipboard(&self) -> Option<(&Vec<PathBuf>, &ClipboardOperation)> {
|
||||||
|
self.clipboard_operation
|
||||||
|
.as_ref()
|
||||||
|
.map(|op| (&self.clipboard_paths, op))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_tree_lines(ui: &mut Ui, depth: usize, is_last: bool, extend_to_icon: bool) {
|
||||||
|
if depth == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let line_height = ui.text_style_height(&egui::TextStyle::Body);
|
||||||
|
let char_width = 8.0;
|
||||||
|
let base_width = (depth as f32 * 3.0 * char_width).ceil();
|
||||||
|
|
||||||
|
let (rect, _) = if extend_to_icon {
|
||||||
|
ui.allocate_at_least(
|
||||||
|
egui::vec2(base_width + 16.0, line_height),
|
||||||
|
egui::Sense::hover(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ui.allocate_at_least(egui::vec2(base_width, line_height), egui::Sense::hover())
|
||||||
|
};
|
||||||
|
|
||||||
|
let painter = ui.painter();
|
||||||
|
let stroke = egui::Stroke::new(1.0, egui::Color32::from_gray(128));
|
||||||
|
|
||||||
|
let mut x_offset = rect.left();
|
||||||
|
|
||||||
|
for i in 0..depth {
|
||||||
|
if i == depth - 1 {
|
||||||
|
let line_x = x_offset + char_width * 1.5;
|
||||||
|
|
||||||
|
if is_last {
|
||||||
|
painter.line_segment(
|
||||||
|
[
|
||||||
|
egui::pos2(line_x, rect.top() - line_height * 0.5),
|
||||||
|
egui::pos2(line_x, rect.bottom() - line_height * 0.5),
|
||||||
|
],
|
||||||
|
stroke,
|
||||||
|
);
|
||||||
|
let end_x = if extend_to_icon {
|
||||||
|
rect.right() + char_width
|
||||||
|
} else {
|
||||||
|
x_offset + char_width * 4.0
|
||||||
|
};
|
||||||
|
painter.line_segment(
|
||||||
|
[
|
||||||
|
egui::pos2(line_x, rect.bottom() - line_height * 0.5),
|
||||||
|
egui::pos2(end_x, rect.bottom() - line_height * 0.5),
|
||||||
|
],
|
||||||
|
stroke,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
painter.line_segment(
|
||||||
|
[
|
||||||
|
egui::pos2(line_x, rect.top() - line_height * 0.5),
|
||||||
|
egui::pos2(line_x, rect.bottom() + line_height * 0.5),
|
||||||
|
],
|
||||||
|
stroke,
|
||||||
|
);
|
||||||
|
let end_x = if extend_to_icon {
|
||||||
|
rect.right() + char_width
|
||||||
|
} else {
|
||||||
|
x_offset + char_width * 4.0
|
||||||
|
};
|
||||||
|
painter.line_segment(
|
||||||
|
[
|
||||||
|
egui::pos2(line_x, rect.bottom() - line_height * 0.5),
|
||||||
|
egui::pos2(end_x, rect.bottom() - line_height * 0.5),
|
||||||
|
],
|
||||||
|
stroke,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let line_x = x_offset + char_width * 1.5;
|
||||||
|
painter.line_segment(
|
||||||
|
[
|
||||||
|
egui::pos2(line_x, rect.top() - line_height * 0.5),
|
||||||
|
egui::pos2(line_x, rect.bottom() + line_height * 0.5),
|
||||||
|
],
|
||||||
|
stroke,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
x_offset += char_width * 3.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_text_file(path: &Path) -> bool {
|
||||||
|
let language = crate::ui::central_panel::languages::get_language_from_extension(Some(path));
|
||||||
|
|
||||||
|
if language != "txt" {
|
||||||
|
true
|
||||||
|
} else if let Some(extension) = path.extension().and_then(|s| s.to_str()) {
|
||||||
|
matches!(
|
||||||
|
extension.to_lowercase().as_str(),
|
||||||
|
"txt" | "gitignore" | "conf" | "cfg" | "ini" | "log" | "csv" | "tsv"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
if let Ok(metadata) = fs::metadata(path) {
|
||||||
|
metadata.len() < 1024 * 1024
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_show_entry(path: &Path, app: &TextEditor, gitignore_patterns: &[String]) -> bool {
|
||||||
|
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||||
|
|
||||||
|
// Check if it's a hidden file
|
||||||
|
if !app.show_hidden_files && file_name.starts_with('.') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it matches gitignore patterns
|
||||||
|
if app.follow_git && !gitignore_patterns.is_empty() {
|
||||||
|
let relative_path = path
|
||||||
|
.strip_prefix(&app.file_tree_root.as_ref().unwrap_or(&PathBuf::from("/")))
|
||||||
|
.unwrap_or(path);
|
||||||
|
|
||||||
|
for pattern in gitignore_patterns {
|
||||||
|
if matches_gitignore_pattern(relative_path, pattern) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_gitignore_patterns(root_path: &Path) -> Vec<String> {
|
||||||
|
let gitignore_path = root_path.join(".gitignore");
|
||||||
|
if !gitignore_path.exists() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = match fs::File::open(&gitignore_path) {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(_) => return Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let reader = std::io::BufReader::new(file);
|
||||||
|
reader
|
||||||
|
.lines()
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.filter(|line| {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
!trimmed.is_empty() && !trimmed.starts_with('#')
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches_gitignore_pattern(path: &Path, pattern: &str) -> bool {
|
||||||
|
let path_str = path.to_string_lossy();
|
||||||
|
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||||
|
|
||||||
|
// Remove leading slashes from pattern
|
||||||
|
let pattern = pattern.trim_start_matches('/');
|
||||||
|
|
||||||
|
// Handle directory patterns (ending with /)
|
||||||
|
if pattern.ends_with('/') {
|
||||||
|
let dir_pattern = &pattern[..pattern.len() - 1];
|
||||||
|
return path.is_dir() && matches_glob_pattern(&path_str, dir_pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle wildcard patterns
|
||||||
|
if pattern.contains('*') || pattern.contains('?') || pattern.contains('[') {
|
||||||
|
return matches_glob_pattern(&path_str, pattern)
|
||||||
|
|| matches_glob_pattern(file_name, pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exact match
|
||||||
|
path_str == pattern || file_name == pattern
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches_glob_pattern(text: &str, pattern: &str) -> bool {
|
||||||
|
// Split pattern by * to handle simple cases
|
||||||
|
let parts: Vec<&str> = pattern.split('*').collect();
|
||||||
|
|
||||||
|
if parts.len() == 1 {
|
||||||
|
// No wildcards, exact match
|
||||||
|
return text == pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
if pattern == "*" {
|
||||||
|
// Match anything
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if pattern.starts_with('*') && pattern.ends_with('*') && parts.len() == 3 && parts[1].is_empty()
|
||||||
|
{
|
||||||
|
// Pattern like "*text*" - contains match
|
||||||
|
return text.contains(parts[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if pattern.starts_with('*') && !pattern.ends_with('*') {
|
||||||
|
// Pattern like "*suffix" - ends with match
|
||||||
|
return text.ends_with(&pattern[1..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !pattern.starts_with('*') && pattern.ends_with('*') {
|
||||||
|
// Pattern like "prefix*" - starts with match
|
||||||
|
return text.starts_with(&pattern[..pattern.len() - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// More complex patterns - use a simple state machine
|
||||||
|
match_complex_glob(text, pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_complex_glob(text: &str, pattern: &str) -> bool {
|
||||||
|
let text_bytes = text.as_bytes();
|
||||||
|
let pattern_bytes = pattern.as_bytes();
|
||||||
|
|
||||||
|
let mut text_pos = 0;
|
||||||
|
let mut pattern_pos = 0;
|
||||||
|
let mut star_pos = None;
|
||||||
|
|
||||||
|
while text_pos < text_bytes.len() {
|
||||||
|
if pattern_pos < pattern_bytes.len()
|
||||||
|
&& (pattern_bytes[pattern_pos] == text_bytes[text_pos]
|
||||||
|
|| pattern_bytes[pattern_pos] == b'?')
|
||||||
|
{
|
||||||
|
text_pos += 1;
|
||||||
|
pattern_pos += 1;
|
||||||
|
} else if pattern_pos < pattern_bytes.len() && pattern_bytes[pattern_pos] == b'*' {
|
||||||
|
star_pos = Some((text_pos, pattern_pos));
|
||||||
|
pattern_pos += 1;
|
||||||
|
} else if let Some((saved_text_pos, saved_pattern_pos)) = star_pos {
|
||||||
|
// Try to advance text position and retry
|
||||||
|
star_pos = Some((saved_text_pos + 1, saved_pattern_pos));
|
||||||
|
text_pos = saved_text_pos + 1;
|
||||||
|
pattern_pos = saved_pattern_pos + 1;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip remaining wildcards
|
||||||
|
while pattern_pos < pattern_bytes.len() && pattern_bytes[pattern_pos] == b'*' {
|
||||||
|
pattern_pos += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern_pos == pattern_bytes.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_directory_context_menu(
|
||||||
|
ui: &mut Ui,
|
||||||
|
path: &Path,
|
||||||
|
app: &mut TextEditor,
|
||||||
|
ctx: &egui::Context,
|
||||||
|
) {
|
||||||
|
ui.menu_button("New", |ui| {
|
||||||
|
if ui.button("File").clicked() {
|
||||||
|
create_new_file_at_path(path, app, ctx);
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
if ui.button("Directory").clicked() {
|
||||||
|
create_new_directory_at_path(path, app, ctx);
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
if ui.button("Cut").clicked() {
|
||||||
|
app.file_tree_state
|
||||||
|
.set_clipboard(vec![path.to_path_buf()], ClipboardOperation::Cut);
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.button("Copy").clicked() {
|
||||||
|
app.file_tree_state
|
||||||
|
.set_clipboard(vec![path.to_path_buf()], ClipboardOperation::Copy);
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.button("Paste").clicked() {
|
||||||
|
if let Some((paths, operation)) = app.file_tree_state.get_clipboard() {
|
||||||
|
paste_items(paths.clone(), path, *operation, app, ctx);
|
||||||
|
}
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
if ui.button("Delete").clicked() {
|
||||||
|
delete_path(path, app, ctx);
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_file_context_menu(ui: &mut Ui, path: &Path, app: &mut TextEditor, ctx: &egui::Context) {
|
||||||
|
ui.menu_button("New", |ui| {
|
||||||
|
if ui.button("File").clicked() {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
create_new_file_at_path(parent, app, ctx);
|
||||||
|
}
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
if ui.button("Directory").clicked() {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
create_new_directory_at_path(parent, app, ctx);
|
||||||
|
}
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
if ui.button("Cut").clicked() {
|
||||||
|
app.file_tree_state
|
||||||
|
.set_clipboard(vec![path.to_path_buf()], ClipboardOperation::Cut);
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.button("Copy").clicked() {
|
||||||
|
app.file_tree_state
|
||||||
|
.set_clipboard(vec![path.to_path_buf()], ClipboardOperation::Copy);
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
if ui.button("Paste").clicked() {
|
||||||
|
if let Some((paths, operation)) = app.file_tree_state.get_clipboard() {
|
||||||
|
paste_items(paths.clone(), parent, *operation, app, ctx);
|
||||||
|
}
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
if ui.button("Delete").clicked() {
|
||||||
|
delete_path(path, app, ctx);
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.button("Rename").clicked() {
|
||||||
|
app.file_tree_state.start_rename(
|
||||||
|
path,
|
||||||
|
path.file_name()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy()
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_tab_index_by_path(path: &Path, app: &TextEditor) -> Option<usize> {
|
||||||
|
app.tabs.iter().position(|tab| {
|
||||||
|
tab.file_path.as_ref().map_or(false, |tab_path| {
|
||||||
|
if tab_path == path {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
match (tab_path.canonicalize(), path.canonicalize()) {
|
||||||
|
(Ok(canonical_tab), Ok(canonical_path)) => canonical_tab == canonical_path,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_new_file_at_path(parent_path: &Path, app: &mut TextEditor, ctx: &egui::Context) {
|
||||||
|
let mut counter = 1;
|
||||||
|
let mut new_file_path = parent_path.join(format!("new_file_{}.txt", counter));
|
||||||
|
|
||||||
|
while new_file_path.exists() {
|
||||||
|
counter += 1;
|
||||||
|
new_file_path = parent_path.join(format!("new_file_{}.txt", counter));
|
||||||
|
}
|
||||||
|
|
||||||
|
match fs::File::create(&new_file_path) {
|
||||||
|
Ok(_) => {
|
||||||
|
app.state_cache = true;
|
||||||
|
// Start rename mode for the newly created file
|
||||||
|
app.file_tree_state.start_rename(&new_file_path, "");
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to create new file: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_new_directory_at_path(parent_path: &Path, app: &mut TextEditor, ctx: &egui::Context) {
|
||||||
|
let mut counter = 1;
|
||||||
|
let mut new_dir_path = parent_path.join(format!("new_directory_{}", counter));
|
||||||
|
|
||||||
|
while new_dir_path.exists() {
|
||||||
|
counter += 1;
|
||||||
|
new_dir_path = parent_path.join(format!("new_directory_{}", counter));
|
||||||
|
}
|
||||||
|
|
||||||
|
match fs::create_dir(&new_dir_path) {
|
||||||
|
Ok(_) => {
|
||||||
|
app.state_cache = true;
|
||||||
|
// Start rename mode for the newly created directory
|
||||||
|
app.file_tree_state.start_rename(&new_dir_path, "");
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to create new directory: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paste_items(
|
||||||
|
source_paths: Vec<PathBuf>,
|
||||||
|
destination_path: &Path,
|
||||||
|
operation: ClipboardOperation,
|
||||||
|
app: &mut TextEditor,
|
||||||
|
ctx: &egui::Context,
|
||||||
|
) {
|
||||||
|
for source_path in source_paths {
|
||||||
|
let file_name = source_path.file_name().unwrap_or_default();
|
||||||
|
let mut target_path = destination_path.join(file_name);
|
||||||
|
let mut counter = 1;
|
||||||
|
|
||||||
|
// Handle name conflicts
|
||||||
|
while target_path.exists() {
|
||||||
|
let stem = source_path
|
||||||
|
.file_stem()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy();
|
||||||
|
let extension = source_path
|
||||||
|
.extension()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy();
|
||||||
|
|
||||||
|
let new_name = if extension.is_empty() {
|
||||||
|
format!("{}_copy_{}", stem, counter)
|
||||||
|
} else {
|
||||||
|
format!("{}_copy_{}.{}", stem, counter, extension)
|
||||||
|
};
|
||||||
|
|
||||||
|
target_path = destination_path.join(new_name);
|
||||||
|
counter += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = match operation {
|
||||||
|
ClipboardOperation::Copy => {
|
||||||
|
if source_path.is_file() {
|
||||||
|
fs::copy(&source_path, &target_path).map(|_| ())
|
||||||
|
} else {
|
||||||
|
copy_directory_recursive(&source_path, &target_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ClipboardOperation::Cut => fs::rename(&source_path, &target_path),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = result {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to {} {} to {}: {}",
|
||||||
|
match operation {
|
||||||
|
ClipboardOperation::Copy => "copy",
|
||||||
|
ClipboardOperation::Cut => "move",
|
||||||
|
},
|
||||||
|
source_path.display(),
|
||||||
|
target_path.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.file_tree_state.clear_clipboard();
|
||||||
|
app.state_cache = true;
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_directory_recursive(source: &Path, destination: &Path) -> Result<(), std::io::Error> {
|
||||||
|
fs::create_dir(destination)?;
|
||||||
|
|
||||||
|
for entry in fs::read_dir(source)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let entry_path = entry.path();
|
||||||
|
let dest_path = destination.join(entry.file_name());
|
||||||
|
|
||||||
|
if entry_path.is_dir() {
|
||||||
|
copy_directory_recursive(&entry_path, &dest_path)?;
|
||||||
|
} else {
|
||||||
|
fs::copy(&entry_path, &dest_path)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_path(path: &Path, app: &mut TextEditor, ctx: &egui::Context) {
|
||||||
|
// Close any open tabs for this file
|
||||||
|
if let Some(tab_index) = find_tab_index_by_path(path, app) {
|
||||||
|
app.tabs.remove(tab_index);
|
||||||
|
if app.active_tab_index >= app.tabs.len() && app.active_tab_index > 0 {
|
||||||
|
app.active_tab_index -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = if path.is_file() {
|
||||||
|
fs::remove_file(path)
|
||||||
|
} else {
|
||||||
|
fs::remove_dir_all(path)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = result {
|
||||||
|
eprintln!("Failed to delete {}: {}", path.display(), e);
|
||||||
|
} else {
|
||||||
|
app.state_cache = true;
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_file_opened(path: &Path, app: &TextEditor) -> bool {
|
||||||
|
find_tab_index_by_path(path, app).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_directory(
|
||||||
|
ui: &mut Ui,
|
||||||
|
path: &Path,
|
||||||
|
depth: usize,
|
||||||
|
is_last: bool,
|
||||||
|
app: &mut TextEditor,
|
||||||
|
ctx: &egui::Context,
|
||||||
|
) -> Option<PathBuf> {
|
||||||
|
let dir_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||||
|
|
||||||
|
let mut clicked_path = None;
|
||||||
|
|
||||||
|
let is_expanded = app.file_tree_state.is_expanded(path);
|
||||||
|
let is_renaming = app.file_tree_state.is_renaming(path);
|
||||||
|
let has_opened_files = app.tabs.iter().any(|tab| {
|
||||||
|
tab.file_path.as_ref().map_or(false, |tab_path| {
|
||||||
|
match (tab_path.canonicalize(), path.canonicalize()) {
|
||||||
|
(Ok(canonical_tab), Ok(canonical_dir)) => {
|
||||||
|
canonical_tab.starts_with(&canonical_dir) && canonical_tab != canonical_dir
|
||||||
|
}
|
||||||
|
_ => tab_path.starts_with(path) && tab_path != path,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
draw_tree_lines(ui, depth, is_last, false);
|
||||||
|
if is_renaming {
|
||||||
|
let text_edit = egui::TextEdit::singleline(&mut app.file_tree_state.rename_text)
|
||||||
|
.desired_width(100.0)
|
||||||
|
.hint_text(dir_name);
|
||||||
|
|
||||||
|
let response = ui.add(text_edit);
|
||||||
|
|
||||||
|
if ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||||
|
if let Some((old_path, new_name)) = app.file_tree_state.finish_rename() {
|
||||||
|
if let Err(e) = crate::io::rename_file(app, &old_path, &new_name) {
|
||||||
|
eprintln!("Failed to rename directory: {}", e);
|
||||||
|
}
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
} else if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||||
|
app.file_tree_state.cancel_rename();
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request focus for the rename text edit if we haven't already
|
||||||
|
if !app.file_tree_state.rename_focus_requested {
|
||||||
|
app.focus_manager
|
||||||
|
.request_focus(FocusTarget::FileTreeRename, priorities::HIGH);
|
||||||
|
app.file_tree_state.rename_focus_requested = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let text_color = if has_opened_files {
|
||||||
|
ui.visuals().warn_fg_color
|
||||||
|
//egui::Color32::from_rgb(100, 200, 255) // Light blue for directories with opened files
|
||||||
|
} else {
|
||||||
|
ui.visuals().text_color()
|
||||||
|
};
|
||||||
|
|
||||||
|
let icon = if is_expanded { "📂" } else { "📁" };
|
||||||
|
let display_text = format!("{} {}", icon, dir_name);
|
||||||
|
|
||||||
|
let response = ui.selectable_label(
|
||||||
|
false,
|
||||||
|
egui::RichText::new(display_text).strong().color(text_color),
|
||||||
|
);
|
||||||
|
|
||||||
|
if response.clicked() {
|
||||||
|
app.file_tree_state.toggle_expand(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.context_menu(|ui| {
|
||||||
|
show_directory_context_menu(ui, path, app, ctx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if is_expanded {
|
||||||
|
if let Ok(entries) = fs::read_dir(path) {
|
||||||
|
// Load gitignore patterns if follow_git is enabled
|
||||||
|
let gitignore_patterns = if app.follow_git && path.join(".git").exists() {
|
||||||
|
load_gitignore_patterns(path)
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut entries: Vec<_> = entries
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.filter(|entry| should_show_entry(&entry.path(), app, &gitignore_patterns))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
entries.sort_by(|a, b| {
|
||||||
|
let a_is_dir = a.path().is_dir();
|
||||||
|
let b_is_dir = b.path().is_dir();
|
||||||
|
if a_is_dir == b_is_dir {
|
||||||
|
a.file_name().cmp(&b.file_name())
|
||||||
|
} else if a_is_dir {
|
||||||
|
std::cmp::Ordering::Less
|
||||||
|
} else {
|
||||||
|
std::cmp::Ordering::Greater
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let total_entries = entries.len();
|
||||||
|
for (index, entry) in entries.iter().enumerate() {
|
||||||
|
let entry_path = entry.path();
|
||||||
|
let is_last_entry = index == total_entries - 1;
|
||||||
|
|
||||||
|
if entry_path.is_dir() {
|
||||||
|
if let Some(clicked) =
|
||||||
|
display_directory(ui, &entry_path, depth + 1, is_last_entry, app, ctx)
|
||||||
|
{
|
||||||
|
clicked_path = Some(clicked);
|
||||||
|
}
|
||||||
|
} else if is_text_file(&entry_path) {
|
||||||
|
if let Some(clicked) =
|
||||||
|
display_file(ui, &entry_path, depth + 1, is_last_entry, app, ctx)
|
||||||
|
{
|
||||||
|
clicked_path = Some(clicked);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
display_file(ui, &entry_path, depth + 1, is_last_entry, app, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clicked_path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_file(
|
||||||
|
ui: &mut Ui,
|
||||||
|
path: &Path,
|
||||||
|
depth: usize,
|
||||||
|
is_last: bool,
|
||||||
|
app: &mut TextEditor,
|
||||||
|
ctx: &egui::Context,
|
||||||
|
) -> Option<PathBuf> {
|
||||||
|
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||||
|
|
||||||
|
let is_selected = app.file_tree_state.is_selected(path);
|
||||||
|
let is_opened = is_file_opened(path, app);
|
||||||
|
let is_renaming = app.file_tree_state.is_renaming(path);
|
||||||
|
|
||||||
|
let mut clicked_path = None;
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
draw_tree_lines(ui, depth, is_last, true);
|
||||||
|
|
||||||
|
if is_renaming {
|
||||||
|
let text_edit = egui::TextEdit::singleline(&mut app.file_tree_state.rename_text)
|
||||||
|
.desired_width(100.0)
|
||||||
|
.hint_text(file_name);
|
||||||
|
|
||||||
|
let response = ui.add(text_edit);
|
||||||
|
|
||||||
|
if ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||||
|
if let Some((old_path, new_name)) = app.file_tree_state.finish_rename() {
|
||||||
|
if let Err(e) = crate::io::rename_file(app, &old_path, &new_name) {
|
||||||
|
eprintln!("Failed to rename file: {}", e);
|
||||||
|
}
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
} else if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||||
|
app.file_tree_state.cancel_rename();
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request focus for the rename text edit if we haven't already
|
||||||
|
if !app.file_tree_state.rename_focus_requested {
|
||||||
|
app.focus_manager
|
||||||
|
.request_focus(FocusTarget::FileTreeRename, priorities::HIGH);
|
||||||
|
app.file_tree_state.rename_focus_requested = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let text_color = if is_selected {
|
||||||
|
ui.visuals().error_fg_color
|
||||||
|
} else if is_opened {
|
||||||
|
ui.visuals().warn_fg_color
|
||||||
|
} else {
|
||||||
|
ui.visuals().text_color()
|
||||||
|
};
|
||||||
|
// let icon = get_nerd_font_icon(path.extension().and_then(|s| s.to_str()).unwrap_or(""), path);
|
||||||
|
let icon = "📄";
|
||||||
|
let display_text = format!("{} {}", icon, file_name);
|
||||||
|
let response =
|
||||||
|
ui.selectable_label(false, egui::RichText::new(display_text).color(text_color));
|
||||||
|
|
||||||
|
if response.clicked() {
|
||||||
|
if let Some(tab_index) = find_tab_index_by_path(path, app) {
|
||||||
|
app.switch_to_tab(tab_index);
|
||||||
|
app.file_tree_state.set_selected(Some(path.to_path_buf()));
|
||||||
|
clicked_path = Some(path.to_path_buf());
|
||||||
|
app.file_tree_state.cancel_rename();
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if response.double_clicked() {
|
||||||
|
if is_opened {
|
||||||
|
app.file_tree_state.start_rename(path, file_name);
|
||||||
|
} else {
|
||||||
|
if let Err(e) = crate::io::open_file_from_path(app, path.to_path_buf()) {
|
||||||
|
eprintln!("Failed to open file: {}", e);
|
||||||
|
}
|
||||||
|
app.file_tree_state.set_selected(Some(path.to_path_buf()));
|
||||||
|
clicked_path = Some(path.to_path_buf());
|
||||||
|
}
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
response.context_menu(|ui| {
|
||||||
|
show_file_context_menu(ui, path, app, ctx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
clicked_path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn file_tree(app: &mut TextEditor, ctx: &egui::Context) {
|
||||||
|
let panel = if app.file_tree_side {
|
||||||
|
egui::SidePanel::right("file_tree")
|
||||||
|
} else {
|
||||||
|
egui::SidePanel::left("file_tree")
|
||||||
|
};
|
||||||
|
|
||||||
|
panel
|
||||||
|
.resizable(true)
|
||||||
|
.default_width(150.0)
|
||||||
|
.show_animated(ctx, app.show_file_tree, |ui| {
|
||||||
|
ui.horizontal_top(|ui| {
|
||||||
|
if ui.button("📁").clicked() {
|
||||||
|
if let Some(path) = rfd::FileDialog::new()
|
||||||
|
.set_directory(std::env::current_dir().unwrap_or_default())
|
||||||
|
.pick_folder()
|
||||||
|
{
|
||||||
|
app.file_tree_root = Some(path.clone());
|
||||||
|
app.state_cache = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
egui::ScrollArea::vertical()
|
||||||
|
.auto_shrink([false, false])
|
||||||
|
.show(ui, |ui| {
|
||||||
|
let display_dir = app
|
||||||
|
.file_tree_root
|
||||||
|
.clone()
|
||||||
|
.filter(|path| path.exists())
|
||||||
|
.or_else(|| std::env::current_dir().ok().filter(|path| path.exists()))
|
||||||
|
.or_else(|| {
|
||||||
|
std::env::var("HOME")
|
||||||
|
.ok()
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.filter(|path| path.exists())
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(dir) = display_dir {
|
||||||
|
display_directory(ui, &dir, 0, true, app, ctx);
|
||||||
|
} else {
|
||||||
|
ui.label("Failed to get current directory or home directory");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_nerd_font_icon(extension: &str, path: &Path) -> &'static str {
|
||||||
|
let filename = path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
match filename.to_lowercase().as_str() {
|
||||||
|
"license" | "license.md" | "license.txt" => "", // License file
|
||||||
|
"dockerfile" => "", // Docker
|
||||||
|
"makefile" => "", // Makefile
|
||||||
|
"readme" | "readme.md" => "", // README
|
||||||
|
".gitignore" => "", // Git
|
||||||
|
"cargo.toml" => "", // Rust/Cargo
|
||||||
|
"package.json" => "", // Node/npm
|
||||||
|
"cargo.lock" => "", // Cargo lock
|
||||||
|
_ => match extension.to_lowercase().as_str() {
|
||||||
|
"rs" => "", // Rust
|
||||||
|
"py" => "", // Python
|
||||||
|
"js" => "", // JavaScript
|
||||||
|
"ts" => "", // TypeScript
|
||||||
|
"jsx" | "tsx" => "", // React
|
||||||
|
"html" => "", // HTML
|
||||||
|
"css" => "", // CSS
|
||||||
|
"scss" | "sass" => "", // SASS
|
||||||
|
"json" => "", // JSON
|
||||||
|
"toml" => "", // TOML
|
||||||
|
"yaml" | "yml" => "", // YAML
|
||||||
|
"xml" => "", // XML
|
||||||
|
"c" => "", // C
|
||||||
|
"cpp" | "cxx" | "cc" => "", // C++
|
||||||
|
"h" | "hpp" => "", // Header files
|
||||||
|
"go" => "", // Go
|
||||||
|
"java" => "", // Java
|
||||||
|
"kt" | "kts" => "", // Kotlin
|
||||||
|
"rb" => "", // Ruby
|
||||||
|
"php" => "", // PHP
|
||||||
|
"cs" => "", // C#
|
||||||
|
"swift" => "", // Swift
|
||||||
|
"dart" => "", // Dart
|
||||||
|
"lua" => "", // Lua
|
||||||
|
"sh" | "bash" | "zsh" | "fish" => "", // Shell scripts
|
||||||
|
|
||||||
|
"md" | "markdown" => "", // Markdown
|
||||||
|
"txt" => "", // Text
|
||||||
|
"pdf" => "", // PDF
|
||||||
|
"doc" | "docx" => "", // Word
|
||||||
|
"xls" | "xlsx" => "", // Excel
|
||||||
|
"ppt" | "pptx" => "", // PowerPoint
|
||||||
|
|
||||||
|
"png" | "jpg" | "jpeg" | "gif" | "bmp" => "", // Images
|
||||||
|
"svg" => "", // SVG
|
||||||
|
"mp3" | "wav" | "ogg" | "flac" => "", // Audio
|
||||||
|
"mp4" | "mkv" | "avi" | "mov" | "webm" => "", // Video
|
||||||
|
|
||||||
|
"zip" | "tar" | "gz" | "bz2" | "xz" | "7z" | "rar" => "", // Archives
|
||||||
|
|
||||||
|
"exe" | "msi" => "", // Windows executable
|
||||||
|
"app" | "dmg" => "", // macOS application
|
||||||
|
"db" | "sqlite" | "sqlite3" => "", // Databases
|
||||||
|
"csv" => "", // CSV
|
||||||
|
"ini" | "conf" | "config" => "", // Config files
|
||||||
|
_ => "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
use crate::app::TextEditor;
|
use crate::app::TextEditor;
|
||||||
use crate::ui::constants::*;
|
use crate::ui::constants::*;
|
||||||
|
use crate::ui::focus_manager::{FocusTarget, priorities};
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
|
|
||||||
pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
|
pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||||
@ -59,7 +60,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() {
|
||||||
@ -67,13 +68,13 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if just_opened || focus_requested || app.focus_find {
|
if just_opened || focus_requested || app.focus_find {
|
||||||
response.request_focus();
|
app.focus_manager.request_focus(FocusTarget::FindInput, priorities::HIGH);
|
||||||
app.focus_find = false;
|
app.focus_find = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||||
app.find_next(ctx);
|
app.find_next(ctx);
|
||||||
response.request_focus();
|
app.focus_manager.request_focus(FocusTarget::FindInput, priorities::HIGH);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -84,7 +85,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..."),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
110
src/ui/focus_manager.rs
Normal file
110
src/ui/focus_manager.rs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
use eframe::egui;
|
||||||
|
|
||||||
|
/// Represents the different focusable components in the application
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum FocusTarget {
|
||||||
|
/// Main text editor
|
||||||
|
Editor,
|
||||||
|
/// Find window input field
|
||||||
|
FindInput,
|
||||||
|
/// File tree rename input
|
||||||
|
FileTreeRename,
|
||||||
|
/// Font size input in preferences
|
||||||
|
FontSizeInput,
|
||||||
|
/// Tab width input in preferences
|
||||||
|
TabWidthInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Centralized focus management system to prevent focus conflicts between components
|
||||||
|
pub struct FocusManager {
|
||||||
|
/// The currently requested focus target
|
||||||
|
current_target: Option<FocusTarget>,
|
||||||
|
/// Priority of the current focus request (higher = more important)
|
||||||
|
current_priority: i32,
|
||||||
|
/// Whether focus should be forced (ignore other requests)
|
||||||
|
force_focus: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FocusManager {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FocusManager {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
current_target: None,
|
||||||
|
current_priority: 0,
|
||||||
|
force_focus: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request focus for a specific target with a given priority
|
||||||
|
/// Higher priority requests will override lower priority ones
|
||||||
|
pub fn request_focus(&mut self, target: FocusTarget, priority: i32) {
|
||||||
|
if priority >= self.current_priority || self.force_focus {
|
||||||
|
self.current_target = Some(target);
|
||||||
|
self.current_priority = priority;
|
||||||
|
self.force_focus = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Force focus to a target, ignoring priority (use sparingly)
|
||||||
|
pub fn force_focus(&mut self, target: FocusTarget) {
|
||||||
|
self.current_target = Some(target);
|
||||||
|
self.force_focus = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear the current focus request
|
||||||
|
pub fn clear_focus(&mut self) {
|
||||||
|
self.current_target = None;
|
||||||
|
self.current_priority = 0;
|
||||||
|
self.force_focus = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current focus target
|
||||||
|
pub fn get_current_target(&self) -> Option<FocusTarget> {
|
||||||
|
self.current_target
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a specific target currently has focus
|
||||||
|
pub fn has_focus(&self, target: FocusTarget) -> bool {
|
||||||
|
self.current_target == Some(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply the current focus request to the UI context
|
||||||
|
pub fn apply_focus(&mut self, ctx: &egui::Context) {
|
||||||
|
if let Some(target) = self.current_target {
|
||||||
|
let id = match target {
|
||||||
|
FocusTarget::Editor => egui::Id::new("main_text_editor"),
|
||||||
|
FocusTarget::FindInput => egui::Id::new("find_input"),
|
||||||
|
FocusTarget::FileTreeRename => egui::Id::new("file_tree_rename"),
|
||||||
|
FocusTarget::FontSizeInput => egui::Id::new("font_size_input"),
|
||||||
|
FocusTarget::TabWidthInput => egui::Id::new("tab_width_input"),
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.memory_mut(|mem| {
|
||||||
|
mem.request_focus(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear the request after applying it
|
||||||
|
self.clear_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset focus state at the beginning of each frame
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.current_target = None;
|
||||||
|
self.current_priority = 0;
|
||||||
|
self.force_focus = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Priority levels for focus requests (higher = more important)
|
||||||
|
pub mod priorities {
|
||||||
|
pub const LOW: i32 = 10;
|
||||||
|
pub const NORMAL: i32 = 50;
|
||||||
|
pub const HIGH: i32 = 100;
|
||||||
|
pub const CRITICAL: i32 = 200;
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
use crate::{app::TextEditor, io};
|
use crate::app::TextEditor;
|
||||||
|
use crate::app::actions::ShortcutAction;
|
||||||
use eframe::egui::{self, Frame};
|
use eframe::egui::{self, Frame};
|
||||||
use egui::UiKind;
|
use egui::UiKind;
|
||||||
|
|
||||||
@ -48,25 +49,25 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
ui.menu_button("File", |ui| {
|
ui.menu_button("File", |ui| {
|
||||||
app.menu_interaction_active = true;
|
app.menu_interaction_active = true;
|
||||||
if ui.button("New").clicked() {
|
if ui.button("New").clicked() {
|
||||||
io::new_file(app);
|
app.perform_action(ShortcutAction::NewFile);
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
if ui.button("Open...").clicked() {
|
if ui.button("Open...").clicked() {
|
||||||
io::open_file(app);
|
app.perform_action(ShortcutAction::OpenFile);
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if ui.button("Save").clicked() {
|
if ui.button("Save").clicked() {
|
||||||
io::save_file(app);
|
app.perform_action(ShortcutAction::SaveFile);
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
if ui.button("Save As...").clicked() {
|
if ui.button("Save As...").clicked() {
|
||||||
io::save_as_file(app);
|
app.perform_action(ShortcutAction::SaveAsFile);
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if ui.button("Preferences").clicked() {
|
if ui.button("Preferences").clicked() {
|
||||||
app.show_preferences = true;
|
app.perform_action(ShortcutAction::Preferences);
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
if ui.button("Exit").clicked() {
|
if ui.button("Exit").clicked() {
|
||||||
@ -103,12 +104,19 @@ 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() {
|
||||||
|
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) =
|
if let Some(mut state) =
|
||||||
egui::TextEdit::load_state(ui.ctx(), text_edit_id)
|
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 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),
|
||||||
egui::text::CCursor::new(text_len),
|
egui::text::CCursor::new(text_len),
|
||||||
@ -117,11 +125,44 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
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() {
|
||||||
|
let id_source = active_tab
|
||||||
|
.file_path
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|| "untitled".to_string());
|
||||||
|
let text_edit_id = egui::Id::new("main_text_editor").with(&id_source);
|
||||||
|
if let Some(state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id)
|
||||||
|
{
|
||||||
|
let current_state = (
|
||||||
|
state.cursor.char_range().unwrap_or_default(),
|
||||||
|
active_tab.content.to_string(),
|
||||||
|
);
|
||||||
|
state.undoer().undo(¤t_state).is_some()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.add_enabled(can_undo, egui::Button::new("Undo"))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
if let Some(active_tab) = app.get_active_tab_mut() {
|
||||||
|
let id_source = active_tab
|
||||||
|
.file_path
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|| "untitled".to_string());
|
||||||
|
let text_edit_id =
|
||||||
|
egui::Id::new("main_text_editor").with(&id_source);
|
||||||
if let Some(mut state) =
|
if let Some(mut state) =
|
||||||
egui::TextEdit::load_state(ui.ctx(), text_edit_id)
|
egui::TextEdit::load_state(ui.ctx(), text_edit_id)
|
||||||
{
|
{
|
||||||
@ -137,7 +178,11 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
active_tab.content = content.to_string();
|
active_tab.content = content.to_string();
|
||||||
state.cursor.set_char_range(Some(*cursor_range));
|
state.cursor.set_char_range(Some(*cursor_range));
|
||||||
state.set_undoer(undoer);
|
state.set_undoer(undoer);
|
||||||
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
egui::TextEdit::store_state(
|
||||||
|
ui.ctx(),
|
||||||
|
text_edit_id,
|
||||||
|
state,
|
||||||
|
);
|
||||||
active_tab.update_modified_state();
|
active_tab.update_modified_state();
|
||||||
if app.show_find && !app.find_query.is_empty() {
|
if app.show_find && !app.find_query.is_empty() {
|
||||||
app.update_find_matches();
|
app.update_find_matches();
|
||||||
@ -145,10 +190,43 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
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() {
|
||||||
|
let id_source = active_tab
|
||||||
|
.file_path
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|| "untitled".to_string());
|
||||||
|
let text_edit_id = egui::Id::new("main_text_editor").with(&id_source);
|
||||||
|
if let Some(state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id)
|
||||||
|
{
|
||||||
|
let current_state = (
|
||||||
|
state.cursor.char_range().unwrap_or_default(),
|
||||||
|
active_tab.content.to_string(),
|
||||||
|
);
|
||||||
|
state.undoer().redo(¤t_state).is_some()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.add_enabled(can_redo, egui::Button::new("Redo"))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
if let Some(active_tab) = app.get_active_tab_mut() {
|
||||||
|
let id_source = active_tab
|
||||||
|
.file_path
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|| "untitled".to_string());
|
||||||
|
let text_edit_id =
|
||||||
|
egui::Id::new("main_text_editor").with(&id_source);
|
||||||
if let Some(mut state) =
|
if let Some(mut state) =
|
||||||
egui::TextEdit::load_state(ui.ctx(), text_edit_id)
|
egui::TextEdit::load_state(ui.ctx(), text_edit_id)
|
||||||
{
|
{
|
||||||
@ -164,7 +242,11 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
active_tab.content = content.to_string();
|
active_tab.content = content.to_string();
|
||||||
state.cursor.set_char_range(Some(*cursor_range));
|
state.cursor.set_char_range(Some(*cursor_range));
|
||||||
state.set_undoer(undoer);
|
state.set_undoer(undoer);
|
||||||
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
egui::TextEdit::store_state(
|
||||||
|
ui.ctx(),
|
||||||
|
text_edit_id,
|
||||||
|
state,
|
||||||
|
);
|
||||||
active_tab.update_modified_state();
|
active_tab.update_modified_state();
|
||||||
if app.show_find && !app.find_query.is_empty() {
|
if app.show_find && !app.find_query.is_empty() {
|
||||||
app.update_find_matches();
|
app.update_find_matches();
|
||||||
@ -172,21 +254,23 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.menu_button("View", |ui| {
|
ui.menu_button("View", |ui| {
|
||||||
app.menu_interaction_active = true;
|
app.menu_interaction_active = true;
|
||||||
|
|
||||||
if ui
|
if ui
|
||||||
.checkbox(&mut app.show_line_numbers, "Show Line Numbers")
|
.checkbox(&mut app.syntax_highlighting, "Syntax Highlighting")
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
app.save_config();
|
app.save_config();
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
if ui
|
if ui
|
||||||
.checkbox(&mut app.syntax_highlighting, "Syntax Highlighting")
|
.checkbox(&mut app.show_markdown, "Preview Markdown")
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
app.save_config();
|
app.save_config();
|
||||||
@ -196,27 +280,62 @@ 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_tab_bar, "Hide Tab Bar").clicked() {
|
if ui.checkbox(&mut app.show_hidden_files, "Show Hidden Files").clicked() {
|
||||||
|
app.save_config();
|
||||||
|
ui.close_kind(UiKind::Menu);
|
||||||
|
}
|
||||||
|
ui.menu_button("Layout", |ui| {
|
||||||
|
ui.menu_button("Line Numbers", |ui| {
|
||||||
|
if ui.checkbox(&mut app.show_line_numbers, "Show").clicked() {
|
||||||
|
app.save_config();
|
||||||
|
ui.close_kind(UiKind::Menu);
|
||||||
|
}
|
||||||
|
if ui.radio_value(&mut app.line_side, false, "Left").clicked() {
|
||||||
|
app.save_config();
|
||||||
|
ui.close_kind(UiKind::Menu);
|
||||||
|
}
|
||||||
|
if ui.radio_value(&mut app.line_side, true, "Right").clicked() {
|
||||||
|
app.save_config();
|
||||||
|
ui.close_kind(UiKind::Menu);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.menu_button("File Tree", |ui| {
|
||||||
|
if ui.checkbox(&mut app.show_file_tree, "Show").clicked() {
|
||||||
|
app.save_config();
|
||||||
|
ui.close_kind(UiKind::Menu);
|
||||||
|
}
|
||||||
|
if ui.radio_value(&mut app.file_tree_side, false, "Left").clicked() {
|
||||||
|
app.save_config();
|
||||||
|
ui.close_kind(UiKind::Menu);
|
||||||
|
}
|
||||||
|
if ui.radio_value(&mut app.file_tree_side, true, "Right").clicked() {
|
||||||
|
app.save_config();
|
||||||
|
ui.close_kind(UiKind::Menu);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if ui.checkbox(&mut app.show_tab_bar, "Tab Bar").clicked() {
|
||||||
app.save_config();
|
app.save_config();
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
if ui
|
if ui
|
||||||
.checkbox(&mut app.auto_hide_toolbar, "Auto Hide Toolbar")
|
.checkbox(&mut app.show_bottom_bar, "Bottom Bar")
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
app.save_config();
|
app.save_config();
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
|
if ui.checkbox(&mut app.show_terminal, "Terminal").clicked() {
|
||||||
ui.separator();
|
app.save_config();
|
||||||
|
|
||||||
if ui.button("Reset Zoom").clicked() {
|
|
||||||
app.zoom_factor = 1.0;
|
|
||||||
ctx.set_zoom_factor(1.0);
|
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
|
if ui
|
||||||
ui.separator();
|
.checkbox(&mut app.auto_hide_toolbar, "Hide Toolbar")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
app.save_config();
|
||||||
|
ui.close_kind(UiKind::Menu);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ui.menu_button("Appearance", |ui| {
|
ui.menu_button("Appearance", |ui| {
|
||||||
app.menu_interaction_active = true;
|
app.menu_interaction_active = true;
|
||||||
@ -258,14 +377,18 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
ui.close_kind(UiKind::Menu);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if ui.radio_value(&mut app.line_side, false, "Left").clicked() {
|
|
||||||
app.save_config();
|
if ui.button("Reset Zoom").clicked() {
|
||||||
ui.close_kind(UiKind::Menu);
|
app.zoom_factor = 1.0;
|
||||||
}
|
ctx.set_zoom_factor(1.0);
|
||||||
if ui.radio_value(&mut app.line_side, true, "Right").clicked() {
|
ui.close_kind(UiKind::Menu);
|
||||||
|
}
|
||||||
|
if ui.button("Focus Mode").clicked() {
|
||||||
|
app.focus_mode = true;
|
||||||
app.save_config();
|
app.save_config();
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -281,7 +404,7 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if app.hide_tab_bar {
|
if !app.show_tab_bar {
|
||||||
let tab_title = if let Some(tab) = app.get_active_tab() {
|
let tab_title = if let Some(tab) = app.get_active_tab() {
|
||||||
tab.get_display_title()
|
tab.get_display_title()
|
||||||
} else {
|
} else {
|
||||||
@ -289,10 +412,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,
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
use crate::app::TextEditor;
|
use crate::app::TextEditor;
|
||||||
use crate::ui::constants::*;
|
use crate::ui::constants::*;
|
||||||
|
use crate::ui::focus_manager::{FocusTarget, priorities};
|
||||||
use eframe::egui;
|
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 =
|
||||||
@ -29,9 +30,6 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
})
|
})
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
ui.heading("Editor Settings");
|
|
||||||
ui.add_space(MEDIUM);
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.vertical(|ui|{
|
ui.vertical(|ui|{
|
||||||
if ui
|
if ui
|
||||||
@ -47,28 +45,19 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ui.add_space(SMALL);
|
ui.add_space(SMALL);
|
||||||
if ui
|
|
||||||
.checkbox(&mut app.show_line_numbers, "Show Line Numbers")
|
|
||||||
.changed()
|
|
||||||
{
|
|
||||||
app.save_config();
|
|
||||||
}
|
|
||||||
ui.add_space(SMALL);
|
|
||||||
if ui
|
|
||||||
.checkbox(&mut app.auto_hide_toolbar, "Auto Hide Toolbar")
|
|
||||||
.on_hover_text(
|
|
||||||
"Hide the top bar until you move your mouse to the upper edge",
|
|
||||||
)
|
|
||||||
.changed()
|
|
||||||
{
|
|
||||||
app.save_config();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
if ui.checkbox(&mut app.word_wrap, "Word Wrap").changed() {
|
if ui.checkbox(&mut app.word_wrap, "Word Wrap").changed() {
|
||||||
app.save_config();
|
app.save_config();
|
||||||
}
|
}
|
||||||
ui.add_space(SMALL);
|
ui.add_space(SMALL);
|
||||||
|
if ui.checkbox(&mut app.show_hidden_files, "Show Hidden Files").on_hover_text("Show files and directories starting with '.'").changed() {
|
||||||
|
app.save_config();
|
||||||
|
}
|
||||||
|
ui.add_space(SMALL);
|
||||||
|
if ui.checkbox(&mut app.follow_git, "Git").on_hover_text("Respect .gitignore file").changed() {
|
||||||
|
app.save_config();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.vertical(|ui|{
|
||||||
if ui
|
if ui
|
||||||
.checkbox(&mut app.syntax_highlighting, "Syntax Highlighting")
|
.checkbox(&mut app.syntax_highlighting, "Syntax Highlighting")
|
||||||
.changed()
|
.changed()
|
||||||
@ -77,10 +66,54 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
}
|
}
|
||||||
ui.add_space(SMALL);
|
ui.add_space(SMALL);
|
||||||
if ui
|
if ui
|
||||||
.checkbox(&mut app.hide_tab_bar, "Hide Tab Bar")
|
.checkbox(&mut app.auto_indent, "Auto Indent")
|
||||||
.on_hover_text(
|
.on_hover_text("Automatically indent new lines to match the previous line")
|
||||||
"Hide the tab bar and show tab title in menu bar instead",
|
.changed()
|
||||||
)
|
{
|
||||||
|
app.save_config();
|
||||||
|
}
|
||||||
|
ui.add_space(SMALL);
|
||||||
|
if ui.checkbox(&mut app.show_markdown, "Preview Markdown").changed() {
|
||||||
|
app.save_config();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(MEDIUM);
|
||||||
|
ui.separator();
|
||||||
|
ui.add_space(SMALL);
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
if ui.checkbox(&mut app.show_line_numbers, "Line Numbers").changed() {
|
||||||
|
app.save_config();
|
||||||
|
}
|
||||||
|
ui.add_space(SMALL);
|
||||||
|
ui.radio_value(&mut app.line_side, false, "Left");
|
||||||
|
ui.add_space(SMALL);
|
||||||
|
ui.radio_value(&mut app.line_side, true, "Right");
|
||||||
|
});
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
if ui.checkbox(&mut app.show_file_tree, "File Tree").changed() {
|
||||||
|
app.save_config();
|
||||||
|
}
|
||||||
|
ui.add_space(SMALL);
|
||||||
|
ui.radio_value(&mut app.file_tree_side, false, "Left");
|
||||||
|
ui.add_space(SMALL);
|
||||||
|
ui.radio_value(&mut app.file_tree_side, true, "Right");
|
||||||
|
});
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
if ui.checkbox(&mut app.show_tab_bar, "Tab Bar").changed() {
|
||||||
|
app.save_config();
|
||||||
|
}
|
||||||
|
ui.add_space(SMALL);
|
||||||
|
if ui.checkbox(&mut app.show_bottom_bar, "Bottom Bar").changed() {
|
||||||
|
app.save_config();
|
||||||
|
}
|
||||||
|
ui.add_space(SMALL);
|
||||||
|
if ui
|
||||||
|
.checkbox(&mut app.auto_hide_toolbar, "Hide Toolbar")
|
||||||
|
.on_hover_text("Hide the toolbar until cursor moves to top of the window")
|
||||||
.changed()
|
.changed()
|
||||||
{
|
{
|
||||||
app.save_config();
|
app.save_config();
|
||||||
@ -88,16 +121,14 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(SMALL);
|
|
||||||
ui.separator();
|
|
||||||
ui.add_space(LARGE);
|
|
||||||
ui.heading("Font Settings");
|
|
||||||
ui.add_space(MEDIUM);
|
ui.add_space(MEDIUM);
|
||||||
|
ui.separator();
|
||||||
|
ui.add_space(SMALL);
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
ui.label("Font Family:");
|
ui.label("Font Family:");
|
||||||
ui.add_space(SMALL);
|
ui.add_space(MEDIUM);
|
||||||
ui.label("Font Size:");
|
ui.label("Font Size:");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -149,10 +180,10 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
app.font_size_input = Some(font_size_text.to_owned());
|
app.font_size_input = Some(font_size_text.to_owned());
|
||||||
|
|
||||||
if response.clicked() {
|
if response.clicked() {
|
||||||
response.request_focus();
|
app.focus_manager.request_focus(FocusTarget::FontSizeInput, priorities::NORMAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.label("px");
|
ui.label("pt");
|
||||||
|
|
||||||
if response.lost_focus() {
|
if response.lost_focus() {
|
||||||
if let Ok(new_size) = font_size_text.parse::<f32>() {
|
if let Ok(new_size) = font_size_text.parse::<f32>() {
|
||||||
@ -170,6 +201,50 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.label("Tab Char:").on_hover_text("Use tab character instead of spaces for indentation");
|
||||||
|
ui.add_space(MEDIUM);
|
||||||
|
ui.label("Tab Width:").on_hover_text("Preferred number of spaces for indentation");
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
if ui.checkbox(&mut app.tab_char, "").changed() {
|
||||||
|
app.save_config();
|
||||||
|
}
|
||||||
|
ui.add_space(SMALL * 1.5);
|
||||||
|
if app.tab_width_input.is_none() {
|
||||||
|
app.tab_width_input = Some(app.tab_width.to_string());
|
||||||
|
}
|
||||||
|
let mut tab_width_text = app
|
||||||
|
.tab_width_input
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&"4".to_string())
|
||||||
|
.to_owned();
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
let response = ui.add(
|
||||||
|
egui::TextEdit::singleline(&mut tab_width_text)
|
||||||
|
.desired_width(FONT_SIZE_INPUT_WIDTH)
|
||||||
|
.hint_text("4").id(egui::Id::new("tab_width_input")),
|
||||||
|
);
|
||||||
|
|
||||||
|
app.tab_width_input = Some(tab_width_text.to_owned());
|
||||||
|
|
||||||
|
if response.clicked() {
|
||||||
|
app.focus_manager.request_focus(FocusTarget::TabWidthInput, priorities::NORMAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.lost_focus() {
|
||||||
|
if let Ok(new_width) = tab_width_text.parse::<usize>() {
|
||||||
|
let clamped_width = new_width.clamp(1, 8);
|
||||||
|
if app.tab_width != clamped_width {
|
||||||
|
app.tab_width = clamped_width;
|
||||||
|
app.apply_font_settings(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.tab_width_input = None;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(MEDIUM);
|
ui.add_space(MEDIUM);
|
||||||
|
|||||||
288
src/ui/shell_bar.rs
Normal file
288
src/ui/shell_bar.rs
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
use crate::app::TextEditor;
|
||||||
|
use crate::ui::constants::*;
|
||||||
|
use eframe::egui::{self, Frame, Id, ScrollArea, TextEdit};
|
||||||
|
use nix::pty::{Winsize, openpty};
|
||||||
|
use nix::unistd::{ForkResult, close, dup2, execvp, fork, setsid};
|
||||||
|
use std::ffi::CString;
|
||||||
|
use std::os::fd::{AsRawFd, OwnedFd};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
pub struct ShellState {
|
||||||
|
pub pty: Option<PtyHandle>,
|
||||||
|
pub output_buffer: Arc<Mutex<String>>,
|
||||||
|
pub input_buffer: String,
|
||||||
|
pub scroll_to_bottom: bool,
|
||||||
|
pub input_id: Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PtyHandle {
|
||||||
|
pub master: OwnedFd,
|
||||||
|
pub _reader_thread: std::thread::JoinHandle<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ShellState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
pty: None,
|
||||||
|
output_buffer: Arc::new(Mutex::new(String::new())),
|
||||||
|
input_buffer: String::new(),
|
||||||
|
scroll_to_bottom: false,
|
||||||
|
input_id: Id::new("terminal_input"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShellState {
|
||||||
|
pub fn start_shell(&mut self) {
|
||||||
|
if self.pty.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the user's shell
|
||||||
|
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string());
|
||||||
|
|
||||||
|
if let Ok(mut output) = self.output_buffer.lock() {
|
||||||
|
output.push_str(&format!("Starting shell: {}\n", shell));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open PTY
|
||||||
|
let pty_result = openpty(
|
||||||
|
Some(&Winsize {
|
||||||
|
ws_row: 24,
|
||||||
|
ws_col: 80,
|
||||||
|
ws_xpixel: 0,
|
||||||
|
ws_ypixel: 0,
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let pty = match pty_result {
|
||||||
|
Ok(pty) => {
|
||||||
|
if let Ok(mut output) = self.output_buffer.lock() {
|
||||||
|
output.push_str("PTY created successfully\n");
|
||||||
|
}
|
||||||
|
pty
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if let Ok(mut output) = self.output_buffer.lock() {
|
||||||
|
output.push_str(&format!("Failed to create PTY: {}\n", e));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fork to create child process
|
||||||
|
match unsafe { fork() } {
|
||||||
|
Ok(ForkResult::Parent { child }) => {
|
||||||
|
// Parent process
|
||||||
|
if let Ok(mut output) = self.output_buffer.lock() {
|
||||||
|
output.push_str(&format!("Forked child process: {:?}\n", child));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the slave side
|
||||||
|
let _ = close(pty.slave.as_raw_fd());
|
||||||
|
|
||||||
|
let master_fd = pty.master.as_raw_fd();
|
||||||
|
|
||||||
|
if let Ok(mut output) = self.output_buffer.lock() {
|
||||||
|
output.push_str(&format!("Master fd: {}\n", master_fd));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set master to non-blocking mode
|
||||||
|
use nix::fcntl::{FcntlArg, OFlag, fcntl};
|
||||||
|
let _ = fcntl(master_fd, FcntlArg::F_SETFL(OFlag::O_NONBLOCK));
|
||||||
|
|
||||||
|
let output_buffer = Arc::clone(&self.output_buffer);
|
||||||
|
|
||||||
|
// Spawn reader thread that polls the master fd
|
||||||
|
let reader_thread = std::thread::spawn(move || {
|
||||||
|
let mut buf = [0u8; 8192];
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Use nix::unistd::read to read from the fd
|
||||||
|
match nix::unistd::read(master_fd, &mut buf) {
|
||||||
|
Ok(0) => {
|
||||||
|
if let Ok(mut output) = output_buffer.lock() {
|
||||||
|
output.push_str("\n[EOF from shell]\n");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Ok(n) => {
|
||||||
|
let text = String::from_utf8_lossy(&buf[..n]).to_string();
|
||||||
|
if let Ok(mut output) = output_buffer.lock() {
|
||||||
|
output.push_str(&text);
|
||||||
|
if output.len() > 100_000 {
|
||||||
|
output.drain(..50_000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(nix::errno::Errno::EAGAIN)
|
||||||
|
| Err(nix::errno::Errno::EWOULDBLOCK) => {
|
||||||
|
// No data available, sleep briefly
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if let Ok(mut output) = output_buffer.lock() {
|
||||||
|
output.push_str(&format!("\n[Read error: {:?}]\n", e));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.pty = Some(PtyHandle {
|
||||||
|
master: pty.master,
|
||||||
|
_reader_thread: reader_thread,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.scroll_to_bottom = true;
|
||||||
|
}
|
||||||
|
Ok(ForkResult::Child) => {
|
||||||
|
// Child process
|
||||||
|
// Close the master side
|
||||||
|
let _ = close(pty.master.as_raw_fd());
|
||||||
|
|
||||||
|
// Create a new session
|
||||||
|
let _ = setsid();
|
||||||
|
|
||||||
|
let slave_fd = pty.slave.as_raw_fd();
|
||||||
|
|
||||||
|
// Make this PTY the controlling terminal
|
||||||
|
unsafe {
|
||||||
|
libc::ioctl(slave_fd, libc::TIOCSCTTY, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect stdin, stdout, stderr to the slave PTY
|
||||||
|
let _ = dup2(slave_fd, 0); // stdin
|
||||||
|
let _ = dup2(slave_fd, 1); // stdout
|
||||||
|
let _ = dup2(slave_fd, 2); // stderr
|
||||||
|
|
||||||
|
// Close the slave fd since it's been duplicated
|
||||||
|
if slave_fd > 2 {
|
||||||
|
let _ = close(slave_fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set TERM environment variable
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var("TERM", "xterm-256color");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the shell with -i for interactive mode
|
||||||
|
let shell_cstr = CString::new(shell.as_bytes()).unwrap();
|
||||||
|
let arg_i = CString::new("-i").unwrap();
|
||||||
|
let args = [shell_cstr.clone(), arg_i];
|
||||||
|
let _ = execvp(&shell_cstr, &args);
|
||||||
|
|
||||||
|
// If execvp returns, it failed
|
||||||
|
eprintln!("Failed to execute shell");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if let Ok(mut output) = self.output_buffer.lock() {
|
||||||
|
output.push_str(&format!("Fork failed: {}\n", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_input(&mut self, text: &str) {
|
||||||
|
if let Some(ref pty) = self.pty {
|
||||||
|
let input_bytes = format!("{}\n", text).into_bytes();
|
||||||
|
|
||||||
|
// Use nix::unistd::write to write to the fd
|
||||||
|
use std::os::fd::AsFd;
|
||||||
|
let _ = nix::unistd::write(pty.master.as_fd(), &input_bytes);
|
||||||
|
|
||||||
|
self.scroll_to_bottom = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_output(&mut self) {
|
||||||
|
if let Ok(mut output) = self.output_buffer.lock() {
|
||||||
|
output.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn shell_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||||
|
// Auto-start shell on first show
|
||||||
|
if app.show_terminal && app.shell_state.pty.is_none() {
|
||||||
|
app.shell_state.start_shell();
|
||||||
|
}
|
||||||
|
|
||||||
|
let frame = Frame::NONE.fill(ctx.style().visuals.panel_fill);
|
||||||
|
|
||||||
|
egui::TopBottomPanel::bottom("shell_bar")
|
||||||
|
.frame(frame)
|
||||||
|
.min_height(200.0)
|
||||||
|
.default_height(300.0)
|
||||||
|
.resizable(true)
|
||||||
|
.show_animated(ctx, app.show_terminal, |ui| {
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.add_space(SMALL);
|
||||||
|
// Simplified header
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add_space(SMALL);
|
||||||
|
if ui.button("Clear").clicked() {
|
||||||
|
app.shell_state.clear_output();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
// Output area
|
||||||
|
let output_height = ui.available_height() - 35.0;
|
||||||
|
|
||||||
|
ScrollArea::vertical()
|
||||||
|
.auto_shrink([false, false])
|
||||||
|
.stick_to_bottom(app.shell_state.scroll_to_bottom)
|
||||||
|
.max_height(output_height)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.style_mut().override_font_id = Some(egui::FontId::monospace(12.0));
|
||||||
|
|
||||||
|
let output_text = if let Ok(output) = app.shell_state.output_buffer.lock() {
|
||||||
|
if output.is_empty() {
|
||||||
|
"".to_string()
|
||||||
|
} else {
|
||||||
|
output.clone()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.label(output_text);
|
||||||
|
});
|
||||||
|
|
||||||
|
if app.shell_state.scroll_to_bottom {
|
||||||
|
app.shell_state.scroll_to_bottom = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
// Input line
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.style_mut().override_font_id = Some(egui::FontId::monospace(12.0));
|
||||||
|
|
||||||
|
let response = ui.add(
|
||||||
|
TextEdit::singleline(&mut app.shell_state.input_buffer)
|
||||||
|
.id(app.shell_state.input_id)
|
||||||
|
.desired_width(f32::INFINITY),
|
||||||
|
);
|
||||||
|
|
||||||
|
if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||||
|
let input = app.shell_state.input_buffer.clone();
|
||||||
|
app.shell_state.input_buffer.clear();
|
||||||
|
app.shell_state.send_input(&input);
|
||||||
|
// Re-focus after command
|
||||||
|
ui.memory_mut(|mem| mem.request_focus(app.shell_state.input_id));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request repaint when shell is active
|
||||||
|
if app.show_terminal && app.shell_state.pty.is_some() {
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,62 +3,163 @@ use crate::ui::constants::*;
|
|||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
|
|
||||||
fn render_shortcuts_content(ui: &mut egui::Ui) {
|
fn render_shortcuts_content(ui: &mut egui::Ui) {
|
||||||
|
let description_color = ui.visuals().text_color().gamma_multiply(0.8);
|
||||||
|
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.add_space(MEDIUM);
|
||||||
|
|
||||||
|
// Navigation section
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
ui.label(
|
ui.label(
|
||||||
egui::RichText::new("Navigation")
|
egui::RichText::new("Navigation")
|
||||||
.size(UI_HEADER_SIZE)
|
.size(UI_HEADER_SIZE)
|
||||||
.strong(),
|
.strong(),
|
||||||
);
|
);
|
||||||
ui.label(egui::RichText::new("Ctrl + N: New").size(UI_TEXT_SIZE));
|
});
|
||||||
ui.label(egui::RichText::new("Ctrl + O: Open").size(UI_TEXT_SIZE));
|
ui.add_space(MEDIUM);
|
||||||
ui.label(egui::RichText::new("Ctrl + S: Save").size(UI_TEXT_SIZE));
|
|
||||||
ui.label(egui::RichText::new("Ctrl + Shift + S: Save As").size(UI_TEXT_SIZE));
|
|
||||||
ui.label(egui::RichText::new("Ctrl + T: New Tab").size(UI_TEXT_SIZE));
|
|
||||||
ui.label(egui::RichText::new("Ctrl + W: Close Tab").size(UI_TEXT_SIZE));
|
|
||||||
ui.label(egui::RichText::new("Ctrl + Tab: Next Tab").size(UI_TEXT_SIZE));
|
|
||||||
ui.label(egui::RichText::new("Ctrl + Shift + Tab: Last Tab").size(UI_TEXT_SIZE));
|
|
||||||
ui.add_space(VLARGE);
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
ui.label(egui::RichText::new("Editing").size(UI_HEADER_SIZE).strong());
|
ui.horizontal(|ui| {
|
||||||
ui.label(egui::RichText::new("Ctrl + Z: Undo").size(UI_TEXT_SIZE));
|
ui.add_space(MEDIUM);
|
||||||
ui.label(egui::RichText::new("Ctrl + Shift + Z: Redo").size(UI_TEXT_SIZE));
|
ui.columns(2, |columns| {
|
||||||
ui.label(egui::RichText::new("Ctrl + X: Cut").size(UI_TEXT_SIZE));
|
let shortcuts = [
|
||||||
ui.label(egui::RichText::new("Ctrl + C: Copy").size(UI_TEXT_SIZE));
|
("Ctrl + N", "New"),
|
||||||
ui.label(egui::RichText::new("Ctrl + V: Paste").size(UI_TEXT_SIZE));
|
("Ctrl + O", "Open"),
|
||||||
ui.label(egui::RichText::new("Ctrl + A: Select All").size(UI_TEXT_SIZE));
|
("Ctrl + S", "Save"),
|
||||||
ui.label(egui::RichText::new("Ctrl + D: Delete Line").size(UI_TEXT_SIZE));
|
("Ctrl + Shift + S", "Save As"),
|
||||||
ui.label(egui::RichText::new("Ctrl + F: Find").size(UI_TEXT_SIZE));
|
("Ctrl + T", "New Tab"),
|
||||||
ui.label(egui::RichText::new("Ctrl + R: Replace").size(UI_TEXT_SIZE));
|
("Ctrl + W", "Close Tab"),
|
||||||
|
("Ctrl + Tab", "Next Tab"),
|
||||||
|
("Ctrl + Shift + Tab", "Last Tab"),
|
||||||
|
];
|
||||||
|
|
||||||
ui.add_space(VLARGE);
|
for (i, (shortcut, description)) in shortcuts.iter().enumerate() {
|
||||||
ui.separator();
|
let col = i % 2;
|
||||||
ui.label(egui::RichText::new("Views").size(UI_HEADER_SIZE).strong());
|
columns[col].label(
|
||||||
ui.label(egui::RichText::new("Ctrl + L: Toggle Line Numbers").size(UI_TEXT_SIZE));
|
egui::RichText::new(*shortcut)
|
||||||
ui.label(
|
.size(UI_TEXT_SIZE)
|
||||||
egui::RichText::new("Ctrl + Shift + L: Change Line Number Side").size(UI_TEXT_SIZE),
|
.strong()
|
||||||
|
.monospace(),
|
||||||
);
|
);
|
||||||
ui.label(egui::RichText::new("Ctrl + K: Toggle Word Wrap").size(UI_TEXT_SIZE));
|
columns[col].label(
|
||||||
ui.label(egui::RichText::new("Ctrl + H: Toggle Auto Hide Toolbar").size(UI_TEXT_SIZE));
|
egui::RichText::new(*description)
|
||||||
ui.label(egui::RichText::new("Ctrl + P: Preferences").size(UI_TEXT_SIZE));
|
.size(UI_TEXT_SIZE)
|
||||||
ui.label(egui::RichText::new("Ctrl + =/-: Increase/Decrease Font Size").size(UI_TEXT_SIZE));
|
.color(description_color),
|
||||||
ui.label(egui::RichText::new("Ctrl + Shift + =/-: Zoom In/Out").size(UI_TEXT_SIZE));
|
);
|
||||||
// ui.label(
|
|
||||||
// egui::RichText::new("Ctrl + Shift + .: Toggle Vim Mode")
|
// Add space after each complete row (every 2 items)
|
||||||
// .size(14.0)
|
if i % 2 == 1 {
|
||||||
// );
|
columns[0].add_space(MEDIUM);
|
||||||
// ui.label(
|
columns[1].add_space(MEDIUM);
|
||||||
// egui::RichText::new("Ctrl + .: Toggle Vim Mode")
|
}
|
||||||
// .size(14.0)
|
}
|
||||||
// );
|
});
|
||||||
|
});
|
||||||
|
|
||||||
ui.add_space(VLARGE);
|
ui.add_space(VLARGE);
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
ui.add_space(MEDIUM);
|
||||||
|
|
||||||
|
// Editing section
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.label(egui::RichText::new("Editing").size(UI_HEADER_SIZE).strong());
|
||||||
|
});
|
||||||
|
ui.add_space(MEDIUM);
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add_space(MEDIUM);
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
let shortcuts = [
|
||||||
|
("Ctrl + Z", "Undo"),
|
||||||
|
("Ctrl + Shift + Z", "Redo"),
|
||||||
|
("Ctrl + X", "Cut"),
|
||||||
|
("Ctrl + C", "Copy"),
|
||||||
|
("Ctrl + V", "Paste"),
|
||||||
|
("Ctrl + A", "Select All"),
|
||||||
|
("Ctrl + F", "Find"),
|
||||||
|
("Ctrl + R", "Replace"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (i, (shortcut, description)) in shortcuts.iter().enumerate() {
|
||||||
|
let col = i % 2;
|
||||||
|
columns[col].label(
|
||||||
|
egui::RichText::new(*shortcut)
|
||||||
|
.size(UI_TEXT_SIZE)
|
||||||
|
.strong()
|
||||||
|
.monospace(),
|
||||||
|
);
|
||||||
|
columns[col].label(
|
||||||
|
egui::RichText::new(*description)
|
||||||
|
.size(UI_TEXT_SIZE)
|
||||||
|
.color(description_color),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add space after each complete row (every 2 items)
|
||||||
|
if i % 2 == 1 {
|
||||||
|
columns[0].add_space(MEDIUM);
|
||||||
|
columns[1].add_space(MEDIUM);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(VLARGE);
|
||||||
|
ui.separator();
|
||||||
|
ui.add_space(MEDIUM);
|
||||||
|
|
||||||
|
// Views section
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.label(egui::RichText::new("Views").size(UI_HEADER_SIZE).strong());
|
||||||
|
});
|
||||||
|
ui.add_space(MEDIUM);
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add_space(MEDIUM);
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
let shortcuts = [
|
||||||
|
("Ctrl + H", "Toggle Auto Hide Toolbar"),
|
||||||
|
("Ctrl + B", "Toggle Bottom Bar"),
|
||||||
|
("Ctrl + K", "Toggle Word Wrap"),
|
||||||
|
("Ctrl + M", "Toggle Markdown Preview"),
|
||||||
|
("Ctrl + L", "Toggle Line Numbers"),
|
||||||
|
("Ctrl + Shift + L", "Change Line Number Side"),
|
||||||
|
("Ctrl + E", "Toggle File Tree"),
|
||||||
|
("Ctrl + Shift + E", "Toggle File Tree Side"),
|
||||||
|
("Ctrl + =/-", "Increase/Decrease Font Size"),
|
||||||
|
("Ctrl + Shift + =/-", "Zoom In/Out"),
|
||||||
|
("Ctrl + P", "Preferences"),
|
||||||
|
("Ctrl + Alt + F", "Toggle Focus Mode"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (i, (shortcut, description)) in shortcuts.iter().enumerate() {
|
||||||
|
let col = i % 2;
|
||||||
|
columns[col].label(
|
||||||
|
egui::RichText::new(*shortcut)
|
||||||
|
.size(UI_TEXT_SIZE)
|
||||||
|
.strong()
|
||||||
|
.monospace(),
|
||||||
|
);
|
||||||
|
columns[col].label(
|
||||||
|
egui::RichText::new(*description)
|
||||||
|
.size(UI_TEXT_SIZE)
|
||||||
|
.color(description_color),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add space after each complete row (every 2 items)
|
||||||
|
if i % 2 == 1 {
|
||||||
|
columns[0].add_space(MEDIUM);
|
||||||
|
columns[1].add_space(MEDIUM);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(VLARGE);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
@ -96,7 +197,8 @@ pub(crate) fn shortcuts_window(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
ui.add_space(MEDIUM);
|
//ui.add_space(MEDIUM);
|
||||||
|
ui.separator();
|
||||||
let visuals = ui.visuals();
|
let visuals = ui.visuals();
|
||||||
let close_button = egui::Button::new("Close")
|
let close_button = egui::Button::new("Close")
|
||||||
.fill(visuals.widgets.inactive.bg_fill)
|
.fill(visuals.widgets.inactive.bg_fill)
|
||||||
|
|||||||
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