Compare commits
No commits in common. "release" and "0.0.4" have entirely different histories.
22
Cargo.toml
22
Cargo.toml
@ -1,19 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ced"
|
name = "ced"
|
||||||
version = "0.1.3"
|
version = "0.0.4"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
eframe = "0.32"
|
eframe = "0.31"
|
||||||
egui = "0.32"
|
egui = "0.31"
|
||||||
egui_extras = { version = "0.32", features = ["syntect"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
rfd = "0.15"
|
||||||
serde_json = "1.0.141"
|
toml = "0.8"
|
||||||
rfd = "0.15.4"
|
dirs = "5.0"
|
||||||
toml = "0.9.2"
|
libc = "0.2"
|
||||||
dirs = "6.0"
|
|
||||||
libc = "0.2.174"
|
|
||||||
syntect = "5.2.0"
|
|
||||||
plist = "1.7.4"
|
|
||||||
diffy = "0.4.2"
|
|
||||||
uuid = { version = "1.0", features = ["v4"] }
|
|
||||||
|
|||||||
11
README.md
11
README.md
@ -9,7 +9,7 @@ There is a disturbing lack of simple GUI text editors available on Linux nativel
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
* 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.
|
* Opens with a blank slate for quick typing, remember Notepad?
|
||||||
* 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.
|
||||||
@ -39,7 +39,6 @@ sudo install -Dm644 ced.desktop /usr/share/applications/ced.desktop
|
|||||||
Here is an example `config.toml`:
|
Here is an example `config.toml`:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
state_cache = true
|
|
||||||
auto_hide_toolbar = false
|
auto_hide_toolbar = false
|
||||||
show_line_numbers = false
|
show_line_numbers = false
|
||||||
word_wrap = false
|
word_wrap = false
|
||||||
@ -47,18 +46,14 @@ theme = "System"
|
|||||||
line_side = false
|
line_side = false
|
||||||
font_family = "Monospace"
|
font_family = "Monospace"
|
||||||
font_size = 16.0
|
font_size = 16.0
|
||||||
syntax_highlighting = true
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
|--------|---------|-------------|
|
|--------|---------|-------------|
|
||||||
| `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_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`. |
|
||||||
| `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. |
|
||||||
| `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. |
|
||||||
@ -69,7 +64,9 @@ syntax_highlighting = true
|
|||||||
In order of importance.
|
In order of importance.
|
||||||
| Feature | Info |
|
| Feature | Info |
|
||||||
| ------- | ---- |
|
| ------- | ---- |
|
||||||
| **LSP:** | Looking at allowing you to use/attach your own tools for this. |
|
| **Find/Replace:** | In progress. |
|
||||||
|
| **State/Cache:** | A toggleable option to keep an application state and prevent "Quit without saving" warnings. |
|
||||||
|
| **Syntax Highlighting/LSP:** | Looking at allowing you to use/attach your own tools for this. |
|
||||||
| **Choose Font** | More than just Monospace/Proportional. |
|
| **Choose Font** | More than just Monospace/Proportional. |
|
||||||
| **Vim Mode:** | It's in-escapable. |
|
| **Vim Mode:** | It's in-escapable. |
|
||||||
| **CLI Mode:** | 💀 |
|
| **CLI Mode:** | 💀 |
|
||||||
|
|||||||
@ -5,71 +5,28 @@ use super::theme::Theme;
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
#[serde(default = "default_state_cache")]
|
|
||||||
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 auto_hide_tab_bar: bool,
|
||||||
pub hide_tab_bar: bool,
|
|
||||||
#[serde(default = "default_show_line_numbers")]
|
|
||||||
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 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 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(),
|
auto_hide_toolbar: false,
|
||||||
auto_hide_toolbar: default_auto_hide_toolbar(),
|
auto_hide_tab_bar: false,
|
||||||
hide_tab_bar: default_hide_tab_bar(),
|
show_line_numbers: false,
|
||||||
show_line_numbers: default_show_line_numbers(),
|
word_wrap: true,
|
||||||
word_wrap: default_word_wrap(),
|
|
||||||
theme: Theme::default(),
|
theme: Theme::default(),
|
||||||
line_side: default_line_side(),
|
line_side: false,
|
||||||
font_family: default_font_family(),
|
font_family: "Proportional".to_string(),
|
||||||
font_size: default_font_size(),
|
font_size: 14.0,
|
||||||
syntax_highlighting: default_syntax_highlighting(),
|
|
||||||
// vim_mode: false,
|
// vim_mode: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -78,9 +35,9 @@ impl Default for Config {
|
|||||||
impl Config {
|
impl Config {
|
||||||
pub fn config_path() -> Option<PathBuf> {
|
pub fn config_path() -> Option<PathBuf> {
|
||||||
let config_dir = if let Some(config_dir) = dirs::config_dir() {
|
let config_dir = if let Some(config_dir) = dirs::config_dir() {
|
||||||
config_dir.join(env!("CARGO_PKG_NAME"))
|
config_dir.join("ced")
|
||||||
} else if let Some(home_dir) = dirs::home_dir() {
|
} else if let Some(home_dir) = dirs::home_dir() {
|
||||||
home_dir.join(".config").join(env!("CARGO_PKG_NAME"))
|
home_dir.join(".config").join("ced")
|
||||||
} else {
|
} else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
@ -103,23 +60,17 @@ impl Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match std::fs::read_to_string(&config_path) {
|
match std::fs::read_to_string(&config_path) {
|
||||||
Ok(content) => {
|
Ok(content) => match toml::from_str::<Config>(&content) {
|
||||||
let mut config = match toml::from_str::<Config>(&content) {
|
Ok(config) => config,
|
||||||
Ok(config) => config,
|
Err(e) => {
|
||||||
Err(e) => {
|
eprintln!(
|
||||||
eprintln!(
|
"Failed to parse config file {}: {}",
|
||||||
"Failed to parse config file {}: {}",
|
config_path.display(),
|
||||||
config_path.display(),
|
e
|
||||||
e
|
);
|
||||||
);
|
Self::default()
|
||||||
return Self::default();
|
}
|
||||||
}
|
},
|
||||||
};
|
|
||||||
|
|
||||||
let default_config = Self::default();
|
|
||||||
config.merge_with_default(default_config);
|
|
||||||
config
|
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Failed to read config file {}: {}",
|
"Failed to read config file {}: {}",
|
||||||
@ -131,16 +82,6 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge_with_default(&mut self, default: Config) {
|
|
||||||
if self.font_family.is_empty() {
|
|
||||||
self.font_family = default.font_family;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.font_size <= 0.0 {
|
|
||||||
self.font_size = default.font_size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let config_path = Self::config_path().ok_or("Cannot determine config directory")?;
|
let config_path = Self::config_path().ok_or("Cannot determine config directory")?;
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,6 @@ enum ShortcutAction {
|
|||||||
ToggleWordWrap,
|
ToggleWordWrap,
|
||||||
ToggleAutoHideToolbar,
|
ToggleAutoHideToolbar,
|
||||||
ToggleFind,
|
ToggleFind,
|
||||||
FocusFind,
|
|
||||||
NextTab,
|
NextTab,
|
||||||
PrevTab,
|
PrevTab,
|
||||||
PageUp,
|
PageUp,
|
||||||
@ -56,11 +55,6 @@ fn get_shortcuts() -> Vec<ShortcutDefinition> {
|
|||||||
egui::Key::W,
|
egui::Key::W,
|
||||||
ShortcutAction::CloseTab,
|
ShortcutAction::CloseTab,
|
||||||
),
|
),
|
||||||
(
|
|
||||||
egui::Modifiers::CTRL | egui::Modifiers::SHIFT,
|
|
||||||
egui::Key::F,
|
|
||||||
ShortcutAction::FocusFind,
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
egui::Modifiers::CTRL,
|
egui::Modifiers::CTRL,
|
||||||
egui::Key::F,
|
egui::Key::F,
|
||||||
@ -149,7 +143,7 @@ fn get_shortcuts() -> Vec<ShortcutDefinition> {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute_action(action: ShortcutAction, editor: &mut TextEditor) -> bool {
|
fn execute_action(action: ShortcutAction, editor: &mut TextEditor, _ctx: &egui::Context) -> bool {
|
||||||
match action {
|
match action {
|
||||||
ShortcutAction::NewFile => {
|
ShortcutAction::NewFile => {
|
||||||
io::new_file(editor);
|
io::new_file(editor);
|
||||||
@ -173,12 +167,15 @@ fn execute_action(action: ShortcutAction, editor: &mut TextEditor) -> bool {
|
|||||||
}
|
}
|
||||||
ShortcutAction::CloseTab => {
|
ShortcutAction::CloseTab => {
|
||||||
if editor.tabs.len() > 1 {
|
if editor.tabs.len() > 1 {
|
||||||
|
// Check if the current tab has unsaved changes
|
||||||
if let Some(current_tab) = editor.get_active_tab() {
|
if let Some(current_tab) = editor.get_active_tab() {
|
||||||
if current_tab.is_modified {
|
if current_tab.is_modified {
|
||||||
|
// Show dialog for unsaved changes
|
||||||
editor.pending_unsaved_action = Some(
|
editor.pending_unsaved_action = Some(
|
||||||
super::state::UnsavedAction::CloseTab(editor.active_tab_index),
|
super::state::UnsavedAction::CloseTab(editor.active_tab_index),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
// Close tab directly if no unsaved changes
|
||||||
editor.close_tab(editor.active_tab_index);
|
editor.close_tab(editor.active_tab_index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -254,25 +251,13 @@ fn execute_action(action: ShortcutAction, editor: &mut TextEditor) -> bool {
|
|||||||
ShortcutAction::Escape => {
|
ShortcutAction::Escape => {
|
||||||
editor.show_about = false;
|
editor.show_about = false;
|
||||||
editor.show_shortcuts = false;
|
editor.show_shortcuts = false;
|
||||||
if editor.show_find {
|
|
||||||
editor.should_select_current_match = true;
|
|
||||||
}
|
|
||||||
editor.show_find = false;
|
editor.show_find = false;
|
||||||
editor.show_preferences = false;
|
editor.show_preferences = false;
|
||||||
editor.pending_unsaved_action = None;
|
editor.pending_unsaved_action = None;
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
ShortcutAction::ToggleFind => {
|
ShortcutAction::ToggleFind => {
|
||||||
editor.show_find = !editor.show_find;
|
//editor.show_find = !editor.show_find;
|
||||||
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
|
false
|
||||||
}
|
}
|
||||||
ShortcutAction::Preferences => {
|
ShortcutAction::Preferences => {
|
||||||
@ -291,16 +276,16 @@ pub fn handle(editor: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
if i.consume_key(modifiers, key) {
|
if i.consume_key(modifiers, key) {
|
||||||
match action {
|
match action {
|
||||||
ShortcutAction::ZoomIn | ShortcutAction::ZoomOut => {
|
ShortcutAction::ZoomIn | ShortcutAction::ZoomOut => {
|
||||||
font_zoom_occurred = execute_action(action, editor);
|
font_zoom_occurred = execute_action(action, editor, ctx);
|
||||||
}
|
}
|
||||||
ShortcutAction::GlobalZoomIn
|
ShortcutAction::GlobalZoomIn
|
||||||
| ShortcutAction::GlobalZoomOut
|
| ShortcutAction::GlobalZoomOut
|
||||||
| ShortcutAction::ResetZoom => {
|
| ShortcutAction::ResetZoom => {
|
||||||
execute_action(action, editor);
|
execute_action(action, editor, ctx);
|
||||||
global_zoom_occurred = true;
|
global_zoom_occurred = true;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
execute_action(action, editor);
|
execute_action(action, editor, ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -315,9 +300,4 @@ pub fn handle(editor: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
if global_zoom_occurred {
|
if global_zoom_occurred {
|
||||||
ctx.set_zoom_factor(editor.zoom_factor);
|
ctx.set_zoom_factor(editor.zoom_factor);
|
||||||
}
|
}
|
||||||
|
|
||||||
if editor.should_select_current_match {
|
|
||||||
editor.select_current_match(ctx);
|
|
||||||
editor.should_select_current_match = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,6 @@ mod editor;
|
|||||||
mod find;
|
mod find;
|
||||||
mod lifecycle;
|
mod lifecycle;
|
||||||
mod processing;
|
mod processing;
|
||||||
mod state_cache;
|
|
||||||
mod tabs;
|
mod tabs;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
|
|||||||
@ -24,7 +24,7 @@ impl eframe::App for TextEditor {
|
|||||||
|
|
||||||
menu_bar(self, ctx);
|
menu_bar(self, ctx);
|
||||||
|
|
||||||
if !self.hide_tab_bar {
|
if !self.auto_hide_tab_bar {
|
||||||
tab_bar(self, ctx);
|
tab_bar(self, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,6 +46,7 @@ impl eframe::App for TextEditor {
|
|||||||
self.show_unsaved_changes_dialog(ctx);
|
self.show_unsaved_changes_dialog(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the previous find state for next frame
|
||||||
self.prev_show_find = self.show_find;
|
self.prev_show_find = self.show_find;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,69 +1,62 @@
|
|||||||
use super::editor::TextEditor;
|
use super::editor::TextEditor;
|
||||||
use crate::app::config::Config;
|
use crate::app::config::Config;
|
||||||
|
use crate::app::tab::Tab;
|
||||||
use crate::app::theme;
|
use crate::app::theme;
|
||||||
use crate::io;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
impl TextEditor {
|
impl TextEditor {
|
||||||
pub fn from_config(config: Config) -> Self {
|
pub fn from_config(config: Config) -> Self {
|
||||||
Self {
|
Self {
|
||||||
state_cache: config.state_cache,
|
tabs: vec![Tab::new_empty(1)],
|
||||||
|
active_tab_index: 0,
|
||||||
|
tab_counter: 1,
|
||||||
|
show_about: false,
|
||||||
|
show_shortcuts: false,
|
||||||
|
show_find: false,
|
||||||
|
show_preferences: false,
|
||||||
|
pending_unsaved_action: None,
|
||||||
|
force_quit_confirmed: false,
|
||||||
|
clean_quit_requested: false,
|
||||||
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,
|
auto_hide_tab_bar: config.auto_hide_tab_bar,
|
||||||
theme: config.theme,
|
theme: config.theme,
|
||||||
line_side: config.line_side,
|
line_side: config.line_side,
|
||||||
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,
|
font_size_input: None,
|
||||||
..Default::default()
|
zoom_factor: 1.0,
|
||||||
|
menu_interaction_active: false,
|
||||||
|
tab_bar_rect: None,
|
||||||
|
menu_bar_stable_until: None,
|
||||||
|
text_processing_result: std::sync::Arc::new(std::sync::Mutex::new(Default::default())),
|
||||||
|
_processing_thread_handle: None,
|
||||||
|
find_query: String::new(),
|
||||||
|
find_matches: Vec::new(),
|
||||||
|
current_match_index: None,
|
||||||
|
case_sensitive_search: false,
|
||||||
|
prev_show_find: false,
|
||||||
|
// vim_mode: config.vim_mode,
|
||||||
|
|
||||||
|
// Cursor tracking for smart scrolling
|
||||||
|
previous_cursor_position: None,
|
||||||
|
|
||||||
|
// Track previous content for incremental processing
|
||||||
|
previous_content: String::new(),
|
||||||
|
previous_cursor_char_index: None,
|
||||||
|
current_cursor_line: 0,
|
||||||
|
previous_cursor_line: 0,
|
||||||
|
font_settings_changed: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_config_with_context(
|
pub fn from_config_with_context(config: Config, cc: &eframe::CreationContext<'_>) -> Self {
|
||||||
config: Config,
|
|
||||||
cc: &eframe::CreationContext<'_>,
|
|
||||||
initial_paths: Vec<PathBuf>,
|
|
||||||
) -> Self {
|
|
||||||
let mut editor = Self::from_config(config);
|
let mut editor = Self::from_config(config);
|
||||||
|
|
||||||
if let Err(e) = editor.load_state_cache() {
|
|
||||||
eprintln!("Failed to load state cache: {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
if !initial_paths.is_empty() {
|
|
||||||
let mut opened_any = false;
|
|
||||||
|
|
||||||
for path in initial_paths {
|
|
||||||
if path.is_file() {
|
|
||||||
match io::open_file_from_path(&mut editor, path.clone()) {
|
|
||||||
Ok(()) => opened_any = true,
|
|
||||||
Err(e) => eprintln!("Error opening file {}: {}", path.display(), e),
|
|
||||||
}
|
|
||||||
} else if path.is_dir() {
|
|
||||||
match io::open_files_from_directory(&mut editor, path.clone()) {
|
|
||||||
Ok(count) => {
|
|
||||||
opened_any = true;
|
|
||||||
println!("Opened {} files from directory {}", count, path.display());
|
|
||||||
}
|
|
||||||
Err(e) => eprintln!("Error opening directory {}: {}", path.display(), e),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
eprintln!("Warning: Path does not exist: {}", path.display());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if opened_any {
|
|
||||||
editor.active_tab_index = editor.tabs.len().saturating_sub(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
theme::apply(editor.theme, &cc.egui_ctx);
|
theme::apply(editor.theme, &cc.egui_ctx);
|
||||||
|
|
||||||
cc.egui_ctx.options_mut(|o| o.zoom_with_keyboard = false);
|
cc.egui_ctx.options_mut(|o| o.zoom_with_keyboard = false);
|
||||||
|
|
||||||
let mut style = (*cc.egui_ctx.style()).to_owned();
|
let mut style = (*cc.egui_ctx.style()).clone();
|
||||||
style
|
style
|
||||||
.text_styles
|
.text_styles
|
||||||
.insert(egui::TextStyle::Body, egui::FontId::proportional(16.0));
|
.insert(egui::TextStyle::Body, egui::FontId::proportional(16.0));
|
||||||
@ -85,16 +78,14 @@ impl TextEditor {
|
|||||||
|
|
||||||
pub fn get_config(&self) -> Config {
|
pub fn get_config(&self) -> Config {
|
||||||
Config {
|
Config {
|
||||||
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,
|
auto_hide_tab_bar: self.auto_hide_tab_bar,
|
||||||
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,
|
||||||
font_family: self.font_family.to_string(),
|
font_family: self.font_family.clone(),
|
||||||
font_size: self.font_size,
|
font_size: self.font_size,
|
||||||
syntax_highlighting: self.syntax_highlighting,
|
|
||||||
// vim_mode: self.vim_mode,
|
// vim_mode: self.vim_mode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,6 @@ impl Default for TextEditor {
|
|||||||
Self {
|
Self {
|
||||||
tabs: vec![Tab::new_empty(1)],
|
tabs: vec![Tab::new_empty(1)],
|
||||||
active_tab_index: 0,
|
active_tab_index: 0,
|
||||||
state_cache: false,
|
|
||||||
tab_counter: 1,
|
tab_counter: 1,
|
||||||
show_about: false,
|
show_about: false,
|
||||||
show_shortcuts: false,
|
show_shortcuts: false,
|
||||||
@ -20,8 +19,7 @@ impl Default for TextEditor {
|
|||||||
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,
|
auto_hide_tab_bar: true,
|
||||||
syntax_highlighting: false,
|
|
||||||
theme: Theme::default(),
|
theme: Theme::default(),
|
||||||
line_side: false,
|
line_side: false,
|
||||||
font_family: "Proportional".to_string(),
|
font_family: "Proportional".to_string(),
|
||||||
@ -33,22 +31,22 @@ impl Default for TextEditor {
|
|||||||
menu_bar_stable_until: None,
|
menu_bar_stable_until: None,
|
||||||
text_processing_result: Arc::new(Mutex::new(TextProcessingResult::default())),
|
text_processing_result: Arc::new(Mutex::new(TextProcessingResult::default())),
|
||||||
_processing_thread_handle: None,
|
_processing_thread_handle: None,
|
||||||
|
// Find functionality
|
||||||
find_query: String::new(),
|
find_query: String::new(),
|
||||||
replace_query: String::new(),
|
|
||||||
find_matches: Vec::new(),
|
find_matches: Vec::new(),
|
||||||
current_match_index: None,
|
current_match_index: None,
|
||||||
case_sensitive_search: false,
|
case_sensitive_search: false,
|
||||||
show_replace_section: false,
|
|
||||||
prev_show_find: false,
|
prev_show_find: false,
|
||||||
focus_find: false,
|
|
||||||
|
// Cursor tracking for smart scrolling
|
||||||
previous_cursor_position: None,
|
previous_cursor_position: None,
|
||||||
|
|
||||||
|
// Track previous content for incremental processing
|
||||||
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,
|
||||||
previous_cursor_line: 0,
|
previous_cursor_line: 0,
|
||||||
font_settings_changed: false,
|
font_settings_changed: false,
|
||||||
text_needs_processing: false,
|
|
||||||
should_select_current_match: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,9 +13,9 @@ pub enum UnsavedAction {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TextProcessingResult {
|
pub struct TextProcessingResult {
|
||||||
pub line_count: usize,
|
pub line_count: usize,
|
||||||
pub longest_line_index: usize,
|
pub longest_line_index: usize, // Which line is the longest (0-based)
|
||||||
pub longest_line_length: usize,
|
pub longest_line_length: usize, // Character count of the longest line
|
||||||
pub longest_line_pixel_width: f32,
|
pub longest_line_pixel_width: f32, // Actual pixel width of the longest line
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TextProcessingResult {
|
impl Default for TextProcessingResult {
|
||||||
@ -33,8 +33,7 @@ impl Default for TextProcessingResult {
|
|||||||
pub struct TextEditor {
|
pub struct TextEditor {
|
||||||
pub(crate) tabs: Vec<Tab>,
|
pub(crate) tabs: Vec<Tab>,
|
||||||
pub(crate) active_tab_index: usize,
|
pub(crate) active_tab_index: usize,
|
||||||
pub(crate) tab_counter: usize,
|
pub(crate) tab_counter: usize, // Counter for numbering new tabs
|
||||||
pub(crate) state_cache: bool,
|
|
||||||
pub(crate) show_about: bool,
|
pub(crate) show_about: bool,
|
||||||
pub(crate) show_shortcuts: bool,
|
pub(crate) show_shortcuts: bool,
|
||||||
pub(crate) show_find: bool,
|
pub(crate) show_find: bool,
|
||||||
@ -45,8 +44,7 @@ pub struct TextEditor {
|
|||||||
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) auto_hide_tab_bar: bool,
|
||||||
pub(crate) syntax_highlighting: bool,
|
|
||||||
pub(crate) theme: Theme,
|
pub(crate) theme: Theme,
|
||||||
pub(crate) line_side: bool,
|
pub(crate) line_side: bool,
|
||||||
pub(crate) font_family: String,
|
pub(crate) font_family: String,
|
||||||
@ -59,19 +57,18 @@ pub struct TextEditor {
|
|||||||
pub(crate) text_processing_result: Arc<Mutex<TextProcessingResult>>,
|
pub(crate) text_processing_result: Arc<Mutex<TextProcessingResult>>,
|
||||||
pub(crate) _processing_thread_handle: Option<thread::JoinHandle<()>>,
|
pub(crate) _processing_thread_handle: Option<thread::JoinHandle<()>>,
|
||||||
pub(crate) find_query: String,
|
pub(crate) find_query: String,
|
||||||
pub(crate) replace_query: String,
|
pub(crate) find_matches: Vec<(usize, usize)>, // (start_pos, end_pos) byte positions
|
||||||
pub(crate) find_matches: Vec<(usize, usize)>,
|
|
||||||
pub(crate) current_match_index: Option<usize>,
|
pub(crate) current_match_index: Option<usize>,
|
||||||
pub(crate) case_sensitive_search: bool,
|
pub(crate) case_sensitive_search: bool,
|
||||||
pub(crate) show_replace_section: bool,
|
pub(crate) prev_show_find: bool, // Track previous state to detect transitions
|
||||||
pub(crate) prev_show_find: bool,
|
|
||||||
pub(crate) focus_find: bool,
|
// Cursor tracking for smart scrolling
|
||||||
|
pub(crate) previous_cursor_position: Option<usize>,
|
||||||
|
|
||||||
|
// Track previous content for incremental processing
|
||||||
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, // Track current line number incrementally
|
||||||
pub(crate) previous_cursor_line: usize,
|
pub(crate) previous_cursor_line: usize, // Track previous line for comparison
|
||||||
pub(crate) font_settings_changed: bool,
|
pub(crate) font_settings_changed: bool, // Flag to trigger text reprocessing when font changes
|
||||||
pub(crate) text_needs_processing: bool,
|
|
||||||
pub(crate) should_select_current_match: bool,
|
|
||||||
pub(crate) previous_cursor_position: Option<usize>,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
use super::editor::TextEditor;
|
use super::editor::TextEditor;
|
||||||
use eframe::egui;
|
|
||||||
|
|
||||||
impl TextEditor {
|
impl TextEditor {
|
||||||
pub fn update_find_matches(&mut self) {
|
pub fn update_find_matches(&mut self) {
|
||||||
let previous_match_index = self.current_match_index;
|
|
||||||
self.find_matches.clear();
|
self.find_matches.clear();
|
||||||
self.current_match_index = None;
|
self.current_match_index = None;
|
||||||
|
|
||||||
@ -14,60 +12,32 @@ impl TextEditor {
|
|||||||
if let Some(tab) = self.get_active_tab() {
|
if let Some(tab) = self.get_active_tab() {
|
||||||
let content = &tab.content;
|
let content = &tab.content;
|
||||||
let query = if self.case_sensitive_search {
|
let query = if self.case_sensitive_search {
|
||||||
self.find_query.to_owned()
|
self.find_query.clone()
|
||||||
} else {
|
} else {
|
||||||
self.find_query.to_lowercase()
|
self.find_query.to_lowercase()
|
||||||
};
|
};
|
||||||
|
|
||||||
let search_content = if self.case_sensitive_search {
|
let search_content = if self.case_sensitive_search {
|
||||||
content.to_string()
|
content.clone()
|
||||||
} else {
|
} else {
|
||||||
content.to_lowercase()
|
content.to_lowercase()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut start = 0;
|
let mut start = 0;
|
||||||
while start < search_content.len() {
|
while let Some(pos) = search_content[start..].find(&query) {
|
||||||
let search_slice = if search_content.is_char_boundary(start) {
|
let absolute_pos = start + pos;
|
||||||
&search_content[start..]
|
self.find_matches
|
||||||
} else {
|
.push((absolute_pos, absolute_pos + query.len()));
|
||||||
while start < search_content.len() && !search_content.is_char_boundary(start) {
|
start = absolute_pos + 1;
|
||||||
start += 1;
|
|
||||||
}
|
|
||||||
if start >= search_content.len() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
&search_content[start..]
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(pos) = search_slice.find(&query) {
|
|
||||||
let absolute_pos = start + pos;
|
|
||||||
self.find_matches
|
|
||||||
.push((absolute_pos, absolute_pos + query.len()));
|
|
||||||
|
|
||||||
start = absolute_pos + 1;
|
|
||||||
while start < search_content.len() && !search_content.is_char_boundary(start) {
|
|
||||||
start += 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.find_matches.is_empty() {
|
if !self.find_matches.is_empty() {
|
||||||
if let Some(prev_index) = previous_match_index {
|
self.current_match_index = Some(0);
|
||||||
if prev_index < self.find_matches.len() {
|
|
||||||
self.current_match_index = Some(prev_index);
|
|
||||||
} else {
|
|
||||||
self.current_match_index = Some(0);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.current_match_index = Some(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_next(&mut self, ctx: &egui::Context) {
|
pub fn find_next(&mut self) {
|
||||||
if self.find_matches.is_empty() {
|
if self.find_matches.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -77,12 +47,9 @@ impl TextEditor {
|
|||||||
} else {
|
} else {
|
||||||
self.current_match_index = Some(0);
|
self.current_match_index = Some(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.select_current_match(ctx);
|
|
||||||
self.should_select_current_match = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_previous(&mut self, ctx: &egui::Context) {
|
pub fn find_previous(&mut self) {
|
||||||
if self.find_matches.is_empty() {
|
if self.find_matches.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -96,9 +63,6 @@ impl TextEditor {
|
|||||||
} else {
|
} else {
|
||||||
self.current_match_index = Some(0);
|
self.current_match_index = Some(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.select_current_match(ctx);
|
|
||||||
self.should_select_current_match = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_current_match_position(&self) -> Option<(usize, usize)> {
|
pub fn get_current_match_position(&self) -> Option<(usize, usize)> {
|
||||||
@ -108,112 +72,4 @@ impl TextEditor {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_current_match(&self, ctx: &egui::Context) {
|
|
||||||
if let Some((start_byte, end_byte)) = self.get_current_match_position() {
|
|
||||||
if let Some(active_tab) = self.get_active_tab() {
|
|
||||||
let content = &active_tab.content;
|
|
||||||
|
|
||||||
let start_char = Self::safe_slice_to_pos(content, start_byte).chars().count();
|
|
||||||
let end_char = Self::safe_slice_to_pos(content, end_byte).chars().count();
|
|
||||||
|
|
||||||
let text_edit_id = egui::Id::new("main_text_editor");
|
|
||||||
if let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) {
|
|
||||||
let selection_range = egui::text::CCursorRange::two(
|
|
||||||
egui::text::CCursor::new(start_char),
|
|
||||||
egui::text::CCursor::new(end_char),
|
|
||||||
);
|
|
||||||
state.cursor.set_char_range(Some(selection_range));
|
|
||||||
egui::TextEdit::store_state(ctx, text_edit_id, state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn replace_current_match(&mut self, ctx: &egui::Context) {
|
|
||||||
if self.find_query.is_empty() || self.find_matches.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((start_byte, end_byte)) = self.get_current_match_position() {
|
|
||||||
let replace_query = self.replace_query.to_owned();
|
|
||||||
let replacement_end = start_byte + replace_query.len();
|
|
||||||
|
|
||||||
if let Some(active_tab) = self.get_active_tab_mut() {
|
|
||||||
let content = &active_tab.content;
|
|
||||||
|
|
||||||
let mut new_content = content.to_string();
|
|
||||||
new_content.replace_range(start_byte..end_byte, &replace_query);
|
|
||||||
|
|
||||||
active_tab.content = new_content;
|
|
||||||
active_tab.is_modified = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.update_find_matches();
|
|
||||||
|
|
||||||
if let Some(active_tab) = self.get_active_tab() {
|
|
||||||
let replacement_end_char =
|
|
||||||
Self::safe_slice_to_pos(&active_tab.content, replacement_end)
|
|
||||||
.chars()
|
|
||||||
.count();
|
|
||||||
|
|
||||||
let text_edit_id = egui::Id::new("main_text_editor");
|
|
||||||
if let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) {
|
|
||||||
state
|
|
||||||
.cursor
|
|
||||||
.set_char_range(Some(egui::text::CCursorRange::one(
|
|
||||||
egui::text::CCursor::new(replacement_end_char),
|
|
||||||
)));
|
|
||||||
egui::TextEdit::store_state(ctx, text_edit_id, state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn replace_all(&mut self, ctx: &egui::Context) {
|
|
||||||
if self.find_query.is_empty() || self.find_matches.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let find_query = self.find_query.to_owned();
|
|
||||||
let replace_query = self.replace_query.to_owned();
|
|
||||||
let case_sensitive = self.case_sensitive_search;
|
|
||||||
let find_matches = self.find_matches.to_owned();
|
|
||||||
|
|
||||||
if let Some(active_tab) = self.get_active_tab_mut() {
|
|
||||||
let content = &active_tab.content;
|
|
||||||
|
|
||||||
let new_content = if case_sensitive {
|
|
||||||
content.replace(&find_query, &replace_query)
|
|
||||||
} else {
|
|
||||||
let mut result = String::new();
|
|
||||||
let mut last_end = 0;
|
|
||||||
|
|
||||||
for (start_byte, end_byte) in &find_matches {
|
|
||||||
result.push_str(&content[last_end..*start_byte]);
|
|
||||||
result.push_str(&replace_query);
|
|
||||||
last_end = *end_byte;
|
|
||||||
}
|
|
||||||
result.push_str(&content[last_end..]);
|
|
||||||
result
|
|
||||||
};
|
|
||||||
|
|
||||||
active_tab.content = new_content;
|
|
||||||
active_tab.is_modified = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.update_find_matches();
|
|
||||||
|
|
||||||
self.current_match_index = None;
|
|
||||||
|
|
||||||
let text_edit_id = egui::Id::new("main_text_editor");
|
|
||||||
if let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) {
|
|
||||||
state
|
|
||||||
.cursor
|
|
||||||
.set_char_range(Some(egui::text::CCursorRange::one(
|
|
||||||
egui::text::CCursor::new(0),
|
|
||||||
)));
|
|
||||||
egui::TextEdit::store_state(ctx, text_edit_id, state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,27 +10,21 @@ impl TextEditor {
|
|||||||
self.tabs
|
self.tabs
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|tab| tab.is_modified)
|
.filter(|tab| tab.is_modified)
|
||||||
.map(|tab| tab.title.to_owned())
|
.map(|tab| tab.title.clone())
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request_quit(&mut self, ctx: &egui::Context) {
|
pub fn request_quit(&mut self, ctx: &egui::Context) {
|
||||||
if self.has_unsaved_changes() && !self.state_cache {
|
if self.has_unsaved_changes() {
|
||||||
self.pending_unsaved_action = Some(UnsavedAction::Quit);
|
self.pending_unsaved_action = Some(UnsavedAction::Quit);
|
||||||
} else {
|
} else {
|
||||||
self.clean_quit_requested = true;
|
self.clean_quit_requested = true;
|
||||||
if let Err(e) = self.save_state_cache() {
|
|
||||||
eprintln!("Failed to save state cache: {e}");
|
|
||||||
}
|
|
||||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn force_quit(&mut self, ctx: &egui::Context) {
|
pub fn force_quit(&mut self, ctx: &egui::Context) {
|
||||||
self.force_quit_confirmed = true;
|
self.force_quit_confirmed = true;
|
||||||
if let Err(e) = self.save_state_cache() {
|
|
||||||
eprintln!("Failed to save state cache: {e}");
|
|
||||||
}
|
|
||||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,70 +35,75 @@ impl TextEditor {
|
|||||||
let (files_to_list, title, confirmation_text, button_text, action) =
|
let (files_to_list, title, confirmation_text, button_text, action) =
|
||||||
if let Some(action) = &self.pending_unsaved_action {
|
if let Some(action) = &self.pending_unsaved_action {
|
||||||
match action {
|
match action {
|
||||||
UnsavedAction::Quit => {
|
UnsavedAction::Quit => (
|
||||||
let files = self.get_unsaved_files();
|
self.get_unsaved_files(),
|
||||||
let file_plural = if files.len() > 1 { "s" } else { "" };
|
"Unsaved Changes".to_string(),
|
||||||
(
|
"You have unsaved changes.".to_string(),
|
||||||
files,
|
"Quit Without Saving".to_string(),
|
||||||
"Unsaved Changes".to_string(),
|
action.clone(),
|
||||||
format!("File{file_plural} with unsaved changes:"),
|
),
|
||||||
"Quit Without Saving".to_string(),
|
|
||||||
action.to_owned(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
UnsavedAction::CloseTab(tab_index) => {
|
UnsavedAction::CloseTab(tab_index) => {
|
||||||
let file_name = self
|
let file_name = self
|
||||||
.tabs
|
.tabs
|
||||||
.get(*tab_index)
|
.get(*tab_index)
|
||||||
.map_or_else(|| "unknown file".to_string(), |tab| tab.title.to_owned());
|
.map_or_else(|| "unknown file".to_string(), |tab| tab.title.clone());
|
||||||
(
|
(
|
||||||
vec![file_name],
|
vec![file_name],
|
||||||
"Unsaved Changes".to_string(),
|
"Unsaved Changes".to_string(),
|
||||||
"This file has unsaved changes:".to_string(),
|
"The file has unsaved changes.".to_string(),
|
||||||
"Close Without Saving".to_string(),
|
"Close Without Saving".to_string(),
|
||||||
action.to_owned(),
|
action.clone(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return;
|
return; // Should not happen if called correctly
|
||||||
};
|
};
|
||||||
|
|
||||||
let visuals = &ctx.style().visuals;
|
let visuals = &ctx.style().visuals;
|
||||||
let error_color = visuals.error_fg_color;
|
|
||||||
|
|
||||||
egui::Window::new(title)
|
egui::Window::new(title)
|
||||||
.collapsible(false)
|
.collapsible(false)
|
||||||
.resizable(false)
|
.resizable(false)
|
||||||
.anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
|
.anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
|
||||||
|
.frame(egui::Frame {
|
||||||
|
fill: visuals.window_fill,
|
||||||
|
stroke: visuals.window_stroke,
|
||||||
|
corner_radius: egui::CornerRadius::same(8),
|
||||||
|
shadow: visuals.window_shadow,
|
||||||
|
inner_margin: egui::Margin::same(16),
|
||||||
|
outer_margin: egui::Margin::same(0),
|
||||||
|
})
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical(|ui| {
|
||||||
ui.add_space(8.0);
|
|
||||||
ui.label(egui::RichText::new(&confirmation_text).size(14.0));
|
ui.label(egui::RichText::new(&confirmation_text).size(14.0));
|
||||||
ui.add_space(4.0);
|
ui.add_space(8.0);
|
||||||
|
|
||||||
for file in &files_to_list {
|
for file in &files_to_list {
|
||||||
ui.label(egui::RichText::new(file).size(12.0).color(error_color));
|
ui.label(egui::RichText::new(format!("• {file}")).size(18.0).weak());
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.add_space(12.0);
|
ui.add_space(12.0);
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if ui.button("Cancel").clicked() {
|
let cancel_fill = ui.visuals().widgets.inactive.bg_fill;
|
||||||
|
let cancel_stroke = ui.visuals().widgets.inactive.bg_stroke;
|
||||||
|
let cancel_button = egui::Button::new("Cancel")
|
||||||
|
.fill(cancel_fill)
|
||||||
|
.stroke(cancel_stroke);
|
||||||
|
|
||||||
|
if ui.add(cancel_button).clicked() {
|
||||||
cancel_action = true;
|
cancel_action = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
|
|
||||||
if ui
|
let destructive_color = ui.visuals().error_fg_color;
|
||||||
.button(egui::RichText::new(&button_text).color(error_color))
|
let confirm_button = egui::Button::new(&button_text)
|
||||||
.clicked()
|
.fill(destructive_color)
|
||||||
{
|
.stroke(egui::Stroke::new(1.0, destructive_color));
|
||||||
close_action_now = Some(action.to_owned());
|
|
||||||
|
if ui.add(confirm_button).clicked() {
|
||||||
|
close_action_now = Some(action);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(8.0);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -114,17 +113,9 @@ impl TextEditor {
|
|||||||
|
|
||||||
if let Some(action) = close_action_now {
|
if let Some(action) = close_action_now {
|
||||||
match action {
|
match action {
|
||||||
UnsavedAction::Quit => {
|
UnsavedAction::Quit => self.force_quit(ctx),
|
||||||
if let Err(e) = self.save_state_cache() {
|
|
||||||
eprintln!("Failed to save state cache: {e}");
|
|
||||||
}
|
|
||||||
self.force_quit(ctx);
|
|
||||||
}
|
|
||||||
UnsavedAction::CloseTab(tab_index) => {
|
UnsavedAction::CloseTab(tab_index) => {
|
||||||
self.close_tab(tab_index);
|
self.close_tab(tab_index);
|
||||||
if let Err(e) = self.save_state_cache() {
|
|
||||||
eprintln!("Failed to save state cache: {e}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.pending_unsaved_action = None;
|
self.pending_unsaved_action = None;
|
||||||
|
|||||||
@ -2,21 +2,12 @@ use super::editor::{TextEditor, TextProcessingResult};
|
|||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
|
|
||||||
impl TextEditor {
|
impl TextEditor {
|
||||||
pub(crate) fn safe_slice_to_pos(content: &str, pos: usize) -> &str {
|
/// Process text content and find the longest line (only used for initial scan)
|
||||||
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 lines: Vec<&str> = content.lines().collect();
|
let lines: Vec<&str> = content.lines().collect();
|
||||||
|
let line_count = lines.len().max(1);
|
||||||
|
|
||||||
if content.is_empty() {
|
if lines.is_empty() {
|
||||||
self.update_processing_result(TextProcessingResult {
|
self.update_processing_result(TextProcessingResult {
|
||||||
line_count: 1,
|
line_count: 1,
|
||||||
longest_line_index: 0,
|
longest_line_index: 0,
|
||||||
@ -29,16 +20,6 @@ impl TextEditor {
|
|||||||
let mut longest_line_index = 0;
|
let mut longest_line_index = 0;
|
||||||
let mut longest_line_length = 0;
|
let mut longest_line_length = 0;
|
||||||
|
|
||||||
if lines.is_empty() {
|
|
||||||
self.update_processing_result(TextProcessingResult {
|
|
||||||
line_count,
|
|
||||||
longest_line_index: 0,
|
|
||||||
longest_line_length: 0,
|
|
||||||
longest_line_pixel_width: 0.0,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (index, line) in lines.iter().enumerate() {
|
for (index, line) in lines.iter().enumerate() {
|
||||||
let char_count = line.chars().count();
|
let char_count = line.chars().count();
|
||||||
if char_count > longest_line_length {
|
if char_count > longest_line_length {
|
||||||
@ -75,6 +56,7 @@ impl TextEditor {
|
|||||||
self.update_processing_result(result);
|
self.update_processing_result(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Efficiently detect and process line changes without full content iteration
|
||||||
pub fn process_incremental_change(
|
pub fn process_incremental_change(
|
||||||
&mut self,
|
&mut self,
|
||||||
old_content: &str,
|
old_content: &str,
|
||||||
@ -121,6 +103,7 @@ impl TextEditor {
|
|||||||
self.previous_cursor_line = self.current_cursor_line;
|
self.previous_cursor_line = self.current_cursor_line;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calculate the change in cursor line without full iteration
|
||||||
fn calculate_cursor_line_change(
|
fn calculate_cursor_line_change(
|
||||||
&self,
|
&self,
|
||||||
old_content: &str,
|
old_content: &str,
|
||||||
@ -128,12 +111,12 @@ impl TextEditor {
|
|||||||
old_cursor_pos: usize,
|
old_cursor_pos: usize,
|
||||||
new_cursor_pos: usize,
|
new_cursor_pos: usize,
|
||||||
) -> isize {
|
) -> isize {
|
||||||
let old_newlines = Self::safe_slice_to_pos(old_content, old_cursor_pos)
|
let old_newlines = old_content[..old_cursor_pos.min(old_content.len())]
|
||||||
.bytes()
|
.bytes()
|
||||||
.filter(|&b| b == b'\n')
|
.filter(|&b| b == b'\n')
|
||||||
.count();
|
.count();
|
||||||
|
|
||||||
let new_newlines = Self::safe_slice_to_pos(new_content, new_cursor_pos)
|
let new_newlines = new_content[..new_cursor_pos.min(new_content.len())]
|
||||||
.bytes()
|
.bytes()
|
||||||
.filter(|&b| b == b'\n')
|
.filter(|&b| b == b'\n')
|
||||||
.count();
|
.count();
|
||||||
@ -141,6 +124,7 @@ impl TextEditor {
|
|||||||
new_newlines as isize - old_newlines as isize
|
new_newlines as isize - old_newlines as isize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle character replacement (same length change)
|
||||||
fn handle_character_replacement(
|
fn handle_character_replacement(
|
||||||
&mut self,
|
&mut self,
|
||||||
_old_content: &str,
|
_old_content: &str,
|
||||||
@ -160,6 +144,7 @@ impl TextEditor {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle content addition
|
||||||
fn handle_content_addition(
|
fn handle_content_addition(
|
||||||
&mut self,
|
&mut self,
|
||||||
old_content: &str,
|
old_content: &str,
|
||||||
@ -171,6 +156,7 @@ impl TextEditor {
|
|||||||
let min_len = old_content.len().min(new_content.len());
|
let min_len = old_content.len().min(new_content.len());
|
||||||
let mut common_prefix = 0;
|
let mut common_prefix = 0;
|
||||||
let mut common_suffix = 0;
|
let mut common_suffix = 0;
|
||||||
|
|
||||||
for i in 0..min_len {
|
for i in 0..min_len {
|
||||||
if old_content.as_bytes()[i] == new_content.as_bytes()[i] {
|
if old_content.as_bytes()[i] == new_content.as_bytes()[i] {
|
||||||
common_prefix += 1;
|
common_prefix += 1;
|
||||||
@ -197,38 +183,21 @@ impl TextEditor {
|
|||||||
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;
|
||||||
|
self.update_processing_result(current_result);
|
||||||
let addition_start_line = Self::safe_slice_to_pos(old_content, added_start)
|
|
||||||
.bytes()
|
|
||||||
.filter(|&b| b == b'\n')
|
|
||||||
.count();
|
|
||||||
let addition_end_line = Self::safe_slice_to_pos(old_content, added_end)
|
|
||||||
.bytes()
|
|
||||||
.filter(|&b| b == b'\n')
|
|
||||||
.count();
|
|
||||||
|
|
||||||
if current_result.longest_line_index >= addition_start_line
|
|
||||||
&& current_result.longest_line_index <= addition_end_line
|
|
||||||
{
|
|
||||||
self.process_text_for_rendering(new_content, ui);
|
|
||||||
} else {
|
|
||||||
if addition_end_line < current_result.longest_line_index {
|
|
||||||
current_result.longest_line_index += newlines_added;
|
|
||||||
}
|
|
||||||
self.update_processing_result(current_result);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let current_line = self.extract_current_line(new_content, new_cursor_pos);
|
|
||||||
let current_line_length = current_line.chars().count();
|
|
||||||
self.update_line_if_longer(
|
|
||||||
self.current_cursor_line,
|
|
||||||
¤t_line,
|
|
||||||
current_line_length,
|
|
||||||
ui,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let current_line = self.extract_current_line(new_content, new_cursor_pos);
|
||||||
|
let current_line_length = current_line.chars().count();
|
||||||
|
|
||||||
|
self.update_line_if_longer(
|
||||||
|
self.current_cursor_line,
|
||||||
|
¤t_line,
|
||||||
|
current_line_length,
|
||||||
|
ui,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle content removal
|
||||||
fn handle_content_removal(
|
fn handle_content_removal(
|
||||||
&mut self,
|
&mut self,
|
||||||
old_content: &str,
|
old_content: &str,
|
||||||
@ -268,27 +237,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)
|
if self.current_cursor_line <= current_result.longest_line_index {
|
||||||
.bytes()
|
|
||||||
.filter(|&b| b == b'\n')
|
|
||||||
.count();
|
|
||||||
let removal_end_line = Self::safe_slice_to_pos(old_content, removed_end)
|
|
||||||
.bytes()
|
|
||||||
.filter(|&b| b == b'\n')
|
|
||||||
.count();
|
|
||||||
|
|
||||||
if current_result.longest_line_index >= removal_start_line
|
|
||||||
&& current_result.longest_line_index <= removal_end_line
|
|
||||||
{
|
|
||||||
self.process_text_for_rendering(new_content, ui);
|
self.process_text_for_rendering(new_content, ui);
|
||||||
} else {
|
|
||||||
if removal_end_line < current_result.longest_line_index {
|
|
||||||
current_result.longest_line_index = current_result
|
|
||||||
.longest_line_index
|
|
||||||
.saturating_sub(newlines_removed);
|
|
||||||
}
|
|
||||||
self.update_processing_result(current_result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.update_processing_result(current_result);
|
||||||
}
|
}
|
||||||
|
|
||||||
let current_line = self.extract_current_line(new_content, new_cursor_pos);
|
let current_line = self.extract_current_line(new_content, new_cursor_pos);
|
||||||
@ -309,32 +262,24 @@ impl TextEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extract the current line efficiently without full content scan
|
||||||
fn extract_current_line(&self, content: &str, cursor_pos: usize) -> String {
|
fn extract_current_line(&self, content: &str, cursor_pos: usize) -> String {
|
||||||
let bytes = content.as_bytes();
|
let bytes = content.as_bytes();
|
||||||
let safe_cursor_pos = cursor_pos.min(bytes.len());
|
|
||||||
|
|
||||||
let mut line_start = safe_cursor_pos;
|
let mut line_start = cursor_pos;
|
||||||
while line_start > 0 && bytes[line_start - 1] != b'\n' {
|
while line_start > 0 && bytes[line_start - 1] != b'\n' {
|
||||||
line_start -= 1;
|
line_start -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut line_end = safe_cursor_pos;
|
let mut line_end = cursor_pos;
|
||||||
while line_end < bytes.len() && bytes[line_end] != b'\n' {
|
while line_end < bytes.len() && bytes[line_end] != b'\n' {
|
||||||
line_end += 1;
|
line_end += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let line_start_boundary = line_start;
|
content[line_start..line_end].to_string()
|
||||||
let line_end_boundary = line_end;
|
|
||||||
|
|
||||||
if content.is_char_boundary(line_start_boundary)
|
|
||||||
&& content.is_char_boundary(line_end_boundary)
|
|
||||||
{
|
|
||||||
content[line_start_boundary..line_end_boundary].to_string()
|
|
||||||
} else {
|
|
||||||
Self::safe_slice_to_pos(content, line_end_boundary)[line_start_boundary..].to_string()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update longest line info if the current line is longer
|
||||||
fn update_line_if_longer(
|
fn update_line_if_longer(
|
||||||
&mut self,
|
&mut self,
|
||||||
line_index: usize,
|
line_index: usize,
|
||||||
@ -369,13 +314,15 @@ impl TextEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the current text processing result
|
||||||
pub fn get_text_processing_result(&self) -> TextProcessingResult {
|
pub fn get_text_processing_result(&self) -> TextProcessingResult {
|
||||||
self.text_processing_result
|
self.text_processing_result
|
||||||
.lock()
|
.lock()
|
||||||
.map(|result| result.to_owned())
|
.map(|result| result.clone())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update the processing result atomically
|
||||||
fn update_processing_result(&self, result: TextProcessingResult) {
|
fn update_processing_result(&self, result: TextProcessingResult) {
|
||||||
if let Ok(mut processing_result) = self.text_processing_result.lock() {
|
if let Ok(mut processing_result) = self.text_processing_result.lock() {
|
||||||
*processing_result = result;
|
*processing_result = result;
|
||||||
|
|||||||
@ -1,255 +0,0 @@
|
|||||||
use super::editor::TextEditor;
|
|
||||||
use crate::app::tab::{compute_content_hash, Tab};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct CachedTab {
|
|
||||||
pub diff_file: Option<PathBuf>,
|
|
||||||
pub full_content: Option<String>, // This is used for 'new files' that don't have a path
|
|
||||||
pub file_path: Option<PathBuf>,
|
|
||||||
pub is_modified: bool,
|
|
||||||
pub title: String,
|
|
||||||
pub original_content_hash: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct StateCache {
|
|
||||||
pub tabs: Vec<CachedTab>,
|
|
||||||
pub active_tab_index: usize,
|
|
||||||
pub tab_counter: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_diff_file(diff_content: &str) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
|
||||||
let diffs_dir = TextEditor::diffs_cache_dir().ok_or("Cannot determine cache directory")?;
|
|
||||||
std::fs::create_dir_all(&diffs_dir)?;
|
|
||||||
|
|
||||||
let diff_filename = format!("{}.diff", Uuid::new_v4());
|
|
||||||
let diff_path = diffs_dir.join(diff_filename);
|
|
||||||
|
|
||||||
std::fs::write(&diff_path, diff_content)?;
|
|
||||||
Ok(diff_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_diff_file(diff_path: &PathBuf) -> Result<String, Box<dyn std::error::Error>> {
|
|
||||||
Ok(std::fs::read_to_string(diff_path)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&Tab> for CachedTab {
|
|
||||||
fn from(tab: &Tab) -> Self {
|
|
||||||
if let Some(file_path) = &tab.file_path {
|
|
||||||
let original_content = std::fs::read_to_string(file_path).unwrap_or_default();
|
|
||||||
let diff_file = if tab.is_modified {
|
|
||||||
let diff_content = diffy::create_patch(&original_content, &tab.content);
|
|
||||||
match create_diff_file(&diff_content.to_string()) {
|
|
||||||
Ok(path) => Some(path),
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Warning: Failed to create diff file: {}", e);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
|
||||||
diff_file,
|
|
||||||
full_content: None,
|
|
||||||
file_path: tab.file_path.clone(),
|
|
||||||
is_modified: tab.is_modified,
|
|
||||||
title: tab.title.clone(),
|
|
||||||
original_content_hash: tab.original_content_hash,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Self {
|
|
||||||
diff_file: None,
|
|
||||||
full_content: Some(tab.content.clone()),
|
|
||||||
file_path: None,
|
|
||||||
is_modified: tab.is_modified,
|
|
||||||
title: tab.title.clone(),
|
|
||||||
original_content_hash: tab.original_content_hash,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<CachedTab> for Tab {
|
|
||||||
fn from(cached: CachedTab) -> Self {
|
|
||||||
if let Some(file_path) = cached.file_path {
|
|
||||||
let original_content = std::fs::read_to_string(&file_path).unwrap_or_default();
|
|
||||||
let current_content = if let Some(diff_path) = cached.diff_file {
|
|
||||||
match load_diff_file(&diff_path) {
|
|
||||||
Ok(diff_content) => {
|
|
||||||
match diffy::Patch::from_str(&diff_content) {
|
|
||||||
Ok(patch) => match diffy::apply(&original_content, &patch) {
|
|
||||||
Ok(content) => content,
|
|
||||||
Err(_) => {
|
|
||||||
eprintln!("Warning: Failed to apply diff for {}, using original content",
|
|
||||||
file_path.display());
|
|
||||||
original_content
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(_) => {
|
|
||||||
eprintln!(
|
|
||||||
"Warning: Failed to parse diff for {}, using original content",
|
|
||||||
file_path.display()
|
|
||||||
);
|
|
||||||
original_content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!(
|
|
||||||
"Warning: Failed to load diff file {:?}: {}, using original content",
|
|
||||||
diff_path, e
|
|
||||||
);
|
|
||||||
original_content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
original_content
|
|
||||||
};
|
|
||||||
|
|
||||||
let original_hash =
|
|
||||||
compute_content_hash(&std::fs::read_to_string(&file_path).unwrap_or_default());
|
|
||||||
let expected_hash = cached.original_content_hash;
|
|
||||||
|
|
||||||
let mut tab = Tab::new_with_file(current_content, file_path);
|
|
||||||
tab.title = cached.title;
|
|
||||||
|
|
||||||
if original_hash != expected_hash {
|
|
||||||
tab.is_modified = true;
|
|
||||||
} else {
|
|
||||||
tab.is_modified = cached.is_modified;
|
|
||||||
tab.original_content_hash = cached.original_content_hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
tab
|
|
||||||
} else {
|
|
||||||
let content = cached.full_content.unwrap_or_default();
|
|
||||||
let mut tab = Tab::new_empty(1);
|
|
||||||
tab.content = content;
|
|
||||||
tab.title = cached.title;
|
|
||||||
tab.is_modified = cached.is_modified;
|
|
||||||
tab.original_content_hash = cached.original_content_hash;
|
|
||||||
tab
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TextEditor {
|
|
||||||
pub fn state_cache_path() -> Option<PathBuf> {
|
|
||||||
let cache_dir = if let Some(cache_dir) = dirs::cache_dir() {
|
|
||||||
cache_dir.join(env!("CARGO_PKG_NAME"))
|
|
||||||
} else if let Some(home_dir) = dirs::home_dir() {
|
|
||||||
home_dir.join(".cache").join(env!("CARGO_PKG_NAME"))
|
|
||||||
} else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(cache_dir.join("state.json"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn diffs_cache_dir() -> Option<PathBuf> {
|
|
||||||
let cache_dir = if let Some(cache_dir) = dirs::cache_dir() {
|
|
||||||
cache_dir.join(env!("CARGO_PKG_NAME"))
|
|
||||||
} else if let Some(home_dir) = dirs::home_dir() {
|
|
||||||
home_dir.join(".cache").join(env!("CARGO_PKG_NAME"))
|
|
||||||
} else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(cache_dir.join("diffs"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cleanup_orphaned_diffs(
|
|
||||||
active_diff_files: &[PathBuf],
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
if let Some(diffs_dir) = Self::diffs_cache_dir() {
|
|
||||||
if diffs_dir.exists() {
|
|
||||||
for entry in std::fs::read_dir(diffs_dir)? {
|
|
||||||
let entry = entry?;
|
|
||||||
let path = entry.path();
|
|
||||||
if path.extension().and_then(|s| s.to_str()) == Some("diff") {
|
|
||||||
if !active_diff_files.contains(&path) {
|
|
||||||
let _ = std::fs::remove_file(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_state_cache(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
if !self.state_cache {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let cache_path = Self::state_cache_path().ok_or("Cannot determine cache directory")?;
|
|
||||||
|
|
||||||
if !cache_path.exists() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = std::fs::read_to_string(&cache_path)?;
|
|
||||||
let state_cache: StateCache = serde_json::from_str(&content)?;
|
|
||||||
|
|
||||||
if !state_cache.tabs.is_empty() {
|
|
||||||
self.tabs = state_cache.tabs.into_iter().map(Tab::from).collect();
|
|
||||||
self.active_tab_index =
|
|
||||||
std::cmp::min(state_cache.active_tab_index, self.tabs.len() - 1);
|
|
||||||
self.tab_counter = state_cache.tab_counter;
|
|
||||||
self.text_needs_processing = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn save_state_cache(&self) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
if !self.state_cache {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let cache_path = Self::state_cache_path().ok_or("Cannot determine cache directory")?;
|
|
||||||
|
|
||||||
if let Some(parent) = cache_path.parent() {
|
|
||||||
std::fs::create_dir_all(parent)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let state_cache = StateCache {
|
|
||||||
tabs: self.tabs.iter().map(CachedTab::from).collect(),
|
|
||||||
active_tab_index: self.active_tab_index,
|
|
||||||
tab_counter: self.tab_counter,
|
|
||||||
};
|
|
||||||
|
|
||||||
let active_diff_files: Vec<PathBuf> = state_cache
|
|
||||||
.tabs
|
|
||||||
.iter()
|
|
||||||
.filter_map(|tab| tab.diff_file.clone())
|
|
||||||
.collect();
|
|
||||||
let _ = Self::cleanup_orphaned_diffs(&active_diff_files);
|
|
||||||
|
|
||||||
let content = serde_json::to_string_pretty(&state_cache)?;
|
|
||||||
std::fs::write(&cache_path, content)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear_state_cache() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
if let Some(cache_path) = Self::state_cache_path() {
|
|
||||||
if cache_path.exists() {
|
|
||||||
std::fs::remove_file(cache_path)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(diffs_dir) = Self::diffs_cache_dir() {
|
|
||||||
if diffs_dir.exists() {
|
|
||||||
let _ = std::fs::remove_dir_all(diffs_dir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -14,14 +14,6 @@ impl TextEditor {
|
|||||||
self.tab_counter += 1;
|
self.tab_counter += 1;
|
||||||
self.tabs.push(Tab::new_empty(self.tab_counter));
|
self.tabs.push(Tab::new_empty(self.tab_counter));
|
||||||
self.active_tab_index = self.tabs.len() - 1;
|
self.active_tab_index = self.tabs.len() - 1;
|
||||||
if self.show_find && !self.find_query.is_empty() {
|
|
||||||
self.update_find_matches();
|
|
||||||
}
|
|
||||||
self.text_needs_processing = true;
|
|
||||||
|
|
||||||
if let Err(e) = self.save_state_cache() {
|
|
||||||
eprintln!("Failed to save state cache: {e}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn close_tab(&mut self, tab_index: usize) {
|
pub fn close_tab(&mut self, tab_index: usize) {
|
||||||
@ -32,28 +24,12 @@ impl TextEditor {
|
|||||||
} else if self.active_tab_index > tab_index {
|
} else if self.active_tab_index > tab_index {
|
||||||
self.active_tab_index -= 1;
|
self.active_tab_index -= 1;
|
||||||
}
|
}
|
||||||
if self.show_find && !self.find_query.is_empty() {
|
|
||||||
self.update_find_matches();
|
|
||||||
}
|
|
||||||
self.text_needs_processing = true;
|
|
||||||
|
|
||||||
if let Err(e) = self.save_state_cache() {
|
|
||||||
eprintln!("Failed to save state cache: {e}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn switch_to_tab(&mut self, tab_index: usize) {
|
pub fn switch_to_tab(&mut self, tab_index: usize) {
|
||||||
if tab_index < self.tabs.len() {
|
if tab_index < self.tabs.len() {
|
||||||
self.active_tab_index = tab_index;
|
self.active_tab_index = tab_index;
|
||||||
if self.show_find && !self.find_query.is_empty() {
|
|
||||||
self.update_find_matches();
|
|
||||||
}
|
|
||||||
self.text_needs_processing = true;
|
|
||||||
|
|
||||||
if let Err(e) = self.save_state_cache() {
|
|
||||||
eprintln!("Failed to save state cache: {e}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,7 @@ impl TextEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the configured font ID based on the editor's font settings
|
||||||
pub fn get_font_id(&self) -> egui::FontId {
|
pub fn get_font_id(&self) -> egui::FontId {
|
||||||
let font_family = match self.font_family.as_str() {
|
let font_family = match self.font_family.as_str() {
|
||||||
"Monospace" => egui::FontFamily::Monospace,
|
"Monospace" => egui::FontFamily::Monospace,
|
||||||
@ -31,18 +32,20 @@ impl TextEditor {
|
|||||||
egui::FontId::new(self.font_size, font_family)
|
egui::FontId::new(self.font_size, font_family)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Immediately apply theme and save to configuration
|
||||||
pub fn set_theme(&mut self, ctx: &egui::Context) {
|
pub fn set_theme(&mut self, ctx: &egui::Context) {
|
||||||
theme::apply(self.theme, ctx);
|
theme::apply(self.theme, ctx);
|
||||||
self.save_config();
|
self.save_config();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Apply font settings with immediate text reprocessing
|
||||||
pub fn apply_font_settings(&mut self, ctx: &egui::Context) {
|
pub fn apply_font_settings(&mut self, ctx: &egui::Context) {
|
||||||
let font_family = match self.font_family.as_str() {
|
let font_family = match self.font_family.as_str() {
|
||||||
"Monospace" => egui::FontFamily::Monospace,
|
"Monospace" => egui::FontFamily::Monospace,
|
||||||
_ => egui::FontFamily::Proportional,
|
_ => egui::FontFamily::Proportional,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut style = (*ctx.style()).to_owned();
|
let mut style = (*ctx.style()).clone();
|
||||||
style.text_styles.insert(
|
style.text_styles.insert(
|
||||||
egui::TextStyle::Monospace,
|
egui::TextStyle::Monospace,
|
||||||
egui::FontId::new(self.font_size, font_family),
|
egui::FontId::new(self.font_size, font_family),
|
||||||
@ -53,6 +56,24 @@ impl TextEditor {
|
|||||||
self.save_config();
|
self.save_config();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Apply font settings with immediate text reprocessing
|
||||||
|
pub fn apply_font_settings_with_ui(&mut self, ctx: &egui::Context, ui: &egui::Ui) {
|
||||||
|
self.apply_font_settings(ctx);
|
||||||
|
self.reprocess_text_for_font_change(ui);
|
||||||
|
self.font_settings_changed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trigger immediate text reprocessing when font settings change
|
||||||
|
pub fn reprocess_text_for_font_change(&mut self, ui: &egui::Ui) {
|
||||||
|
if let Some(active_tab) = self.get_active_tab() {
|
||||||
|
let content = active_tab.content.clone();
|
||||||
|
if !content.is_empty() {
|
||||||
|
self.process_text_for_rendering(&content, ui);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the available width for the text editor, accounting for line numbers and separator
|
||||||
pub fn calculate_editor_dimensions(&self, ui: &egui::Ui) -> EditorDimensions {
|
pub fn calculate_editor_dimensions(&self, ui: &egui::Ui) -> EditorDimensions {
|
||||||
let total_available_width = ui.available_width();
|
let total_available_width = ui.available_width();
|
||||||
|
|
||||||
@ -96,6 +117,7 @@ impl TextEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calculate the available width for non-word-wrapped content based on processed text data
|
||||||
pub fn calculate_content_based_width(&self, ui: &egui::Ui) -> f32 {
|
pub fn calculate_content_based_width(&self, ui: &egui::Ui) -> f32 {
|
||||||
let processing_result = self.get_text_processing_result();
|
let processing_result = self.get_text_processing_result();
|
||||||
|
|
||||||
@ -103,8 +125,7 @@ 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 + self.font_size;
|
||||||
processing_result.longest_line_pixel_width + (self.font_size * 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.max(dimensions.text_width)
|
||||||
|
|||||||
@ -2,9 +2,8 @@ use std::collections::hash_map::DefaultHasher;
|
|||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub fn compute_content_hash(content: &str) -> u64 {
|
pub fn compute_content_hash(content: &str, hasher: &mut DefaultHasher) -> u64 {
|
||||||
let mut hasher = DefaultHasher::new();
|
content.hash(hasher);
|
||||||
content.hash(&mut hasher);
|
|
||||||
hasher.finish()
|
hasher.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -12,21 +11,26 @@ pub fn compute_content_hash(content: &str) -> u64 {
|
|||||||
pub struct Tab {
|
pub struct Tab {
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub original_content_hash: u64,
|
pub original_content_hash: u64,
|
||||||
|
pub last_content_hash: u64,
|
||||||
pub file_path: Option<PathBuf>,
|
pub file_path: Option<PathBuf>,
|
||||||
pub is_modified: bool,
|
pub is_modified: bool,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
pub hasher: DefaultHasher,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tab {
|
impl Tab {
|
||||||
pub fn new_empty(tab_number: usize) -> Self {
|
pub fn new_empty(tab_number: usize) -> Self {
|
||||||
let content = String::new();
|
let content = String::new();
|
||||||
let hash = compute_content_hash(&content);
|
let mut hasher = DefaultHasher::new();
|
||||||
|
let hash = compute_content_hash(&content, &mut hasher);
|
||||||
Self {
|
Self {
|
||||||
original_content_hash: hash,
|
original_content_hash: hash,
|
||||||
|
last_content_hash: hash,
|
||||||
content,
|
content,
|
||||||
file_path: None,
|
file_path: None,
|
||||||
is_modified: false,
|
is_modified: false,
|
||||||
title: format!("new_{tab_number}"),
|
title: format!("new_{tab_number}"),
|
||||||
|
hasher,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,16 +38,19 @@ impl Tab {
|
|||||||
let title = file_path
|
let title = file_path
|
||||||
.file_name()
|
.file_name()
|
||||||
.and_then(|n| n.to_str())
|
.and_then(|n| n.to_str())
|
||||||
.unwrap_or("UNKNOWN")
|
.unwrap_or("Untitled")
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
let hash = compute_content_hash(&content);
|
let mut hasher = DefaultHasher::new();
|
||||||
|
let hash = compute_content_hash(&content, &mut hasher);
|
||||||
Self {
|
Self {
|
||||||
original_content_hash: hash,
|
original_content_hash: hash,
|
||||||
|
last_content_hash: hash,
|
||||||
content,
|
content,
|
||||||
file_path: Some(file_path),
|
file_path: Some(file_path),
|
||||||
is_modified: false,
|
is_modified: false,
|
||||||
title,
|
title,
|
||||||
|
hasher,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,16 +60,21 @@ impl Tab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_modified_state(&mut self) {
|
pub fn update_modified_state(&mut self) {
|
||||||
|
// Compare current content hash with original content hash to determine if modified
|
||||||
|
// Special case: new_X tabs are only considered modified if they have content
|
||||||
if self.title.starts_with("new_") {
|
if self.title.starts_with("new_") {
|
||||||
self.is_modified = !self.content.is_empty();
|
self.is_modified = !self.content.is_empty();
|
||||||
} else {
|
} else {
|
||||||
let current_hash = compute_content_hash(&self.content);
|
let current_hash = compute_content_hash(&self.content, &mut self.hasher);
|
||||||
self.is_modified = current_hash != self.original_content_hash;
|
self.is_modified = current_hash != self.last_content_hash;
|
||||||
|
self.last_content_hash = current_hash;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mark_as_saved(&mut self) {
|
pub fn mark_as_saved(&mut self) {
|
||||||
self.original_content_hash = compute_content_hash(&self.content);
|
// Update the original content hash to match current content after saving
|
||||||
|
self.original_content_hash = compute_content_hash(&self.content, &mut self.hasher);
|
||||||
|
self.last_content_hash = self.original_content_hash;
|
||||||
self.is_modified = false;
|
self.is_modified = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
129
src/app/theme.rs
129
src/app/theme.rs
@ -1,9 +1,4 @@
|
|||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use plist::{Dictionary, Value};
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use syntect::highlighting::{
|
|
||||||
Color as SyntectColor, Theme as SyntectTheme, ThemeSet, ThemeSettings, UnderlineOption,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize, Default)]
|
||||||
pub enum Theme {
|
pub enum Theme {
|
||||||
@ -199,127 +194,3 @@ fn detect_system_dark_mode() -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn egui_color_to_syntect(color: egui::Color32) -> SyntectColor {
|
|
||||||
SyntectColor {
|
|
||||||
r: color.r(),
|
|
||||||
g: color.g(),
|
|
||||||
b: color.b(),
|
|
||||||
a: color.a(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_code_theme_from_visuals(visuals: &egui::Visuals, font_size: f32) -> ThemeSet {
|
|
||||||
let text_color = visuals.override_text_color.unwrap_or(visuals.text_color());
|
|
||||||
let bg_color = visuals.extreme_bg_color;
|
|
||||||
let selection_color = visuals.selection.bg_fill;
|
|
||||||
let comment_color = blend_colors(text_color, bg_color, 0.6);
|
|
||||||
let keyword_color = if visuals.dark_mode {
|
|
||||||
blend_colors(egui::Color32::from_rgb(100, 149, 237), text_color, 0.8) // CornflowerBlue-like
|
|
||||||
} else {
|
|
||||||
blend_colors(egui::Color32::from_rgb(0, 0, 139), text_color, 0.8) // DarkBlue-like
|
|
||||||
};
|
|
||||||
let string_color = if visuals.dark_mode {
|
|
||||||
blend_colors(egui::Color32::from_rgb(144, 238, 144), text_color, 0.8) // LightGreen-like
|
|
||||||
} else {
|
|
||||||
blend_colors(egui::Color32::from_rgb(0, 128, 0), text_color, 0.8) // Green-like
|
|
||||||
};
|
|
||||||
let number_color = if visuals.dark_mode {
|
|
||||||
blend_colors(egui::Color32::from_rgb(255, 165, 0), text_color, 0.8) // Orange-like
|
|
||||||
} else {
|
|
||||||
blend_colors(egui::Color32::from_rgb(165, 42, 42), text_color, 0.8) // Brown-like
|
|
||||||
};
|
|
||||||
let function_color = if visuals.dark_mode {
|
|
||||||
blend_colors(egui::Color32::from_rgb(255, 20, 147), text_color, 0.8) // DeepPink-like
|
|
||||||
} else {
|
|
||||||
blend_colors(egui::Color32::from_rgb(128, 0, 128), text_color, 0.8) // Purple-like
|
|
||||||
};
|
|
||||||
|
|
||||||
let plist_theme = build_custom_theme_plist(
|
|
||||||
"System",
|
|
||||||
&format!("{:?}", bg_color),
|
|
||||||
&format!("{:?}", text_color),
|
|
||||||
&format!("{:?}", comment_color),
|
|
||||||
&format!("{:?}", string_color),
|
|
||||||
&format!("{:?}", keyword_color),
|
|
||||||
);
|
|
||||||
let file = std::fs::File::create("system.tmTheme").unwrap();
|
|
||||||
let writer = std::io::BufWriter::new(file);
|
|
||||||
|
|
||||||
let _ = plist::to_writer_xml(writer, &plist_theme);
|
|
||||||
|
|
||||||
let loaded_file = std::fs::File::open("system.tmTheme").unwrap();
|
|
||||||
let mut loaded_reader = std::io::BufReader::new(loaded_file);
|
|
||||||
let loaded_theme = ThemeSet::load_from_reader(&mut loaded_reader).unwrap();
|
|
||||||
let mut set = ThemeSet::new();
|
|
||||||
set.add_from_folder(".").unwrap();
|
|
||||||
return set;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_custom_theme_plist(
|
|
||||||
theme_name: &str,
|
|
||||||
background_color: &str,
|
|
||||||
foreground_color: &str,
|
|
||||||
comment_color: &str,
|
|
||||||
string_color: &str,
|
|
||||||
keyword_color: &str,
|
|
||||||
) -> Value {
|
|
||||||
let mut root_dict = Dictionary::new();
|
|
||||||
root_dict.insert("name".to_string(), Value::String(theme_name.to_string()));
|
|
||||||
|
|
||||||
let mut settings_array = Vec::new();
|
|
||||||
|
|
||||||
let mut global_settings_dict = Dictionary::new();
|
|
||||||
let mut inner_global_settings = Dictionary::new();
|
|
||||||
inner_global_settings.insert(
|
|
||||||
"background".to_string(),
|
|
||||||
Value::String(background_color.to_string()),
|
|
||||||
);
|
|
||||||
inner_global_settings.insert(
|
|
||||||
"foreground".to_string(),
|
|
||||||
Value::String(foreground_color.to_string()),
|
|
||||||
);
|
|
||||||
global_settings_dict.insert(
|
|
||||||
"settings".to_string(),
|
|
||||||
Value::Dictionary(inner_global_settings),
|
|
||||||
);
|
|
||||||
settings_array.push(Value::Dictionary(global_settings_dict));
|
|
||||||
|
|
||||||
let mut comment_scope_dict = Dictionary::new();
|
|
||||||
comment_scope_dict.insert("name".to_string(), Value::String("Comment".to_string()));
|
|
||||||
comment_scope_dict.insert("scope".to_string(), Value::String("comment".to_string()));
|
|
||||||
let mut comment_settings = Dictionary::new();
|
|
||||||
comment_settings.insert(
|
|
||||||
"foreground".to_string(),
|
|
||||||
Value::String(comment_color.to_string()),
|
|
||||||
);
|
|
||||||
comment_settings.insert("fontStyle".to_string(), Value::String("italic".to_string()));
|
|
||||||
comment_scope_dict.insert("settings".to_string(), Value::Dictionary(comment_settings));
|
|
||||||
settings_array.push(Value::Dictionary(comment_scope_dict));
|
|
||||||
|
|
||||||
let mut string_scope_dict = Dictionary::new();
|
|
||||||
string_scope_dict.insert("name".to_string(), Value::String("String".to_string()));
|
|
||||||
string_scope_dict.insert("scope".to_string(), Value::String("string".to_string()));
|
|
||||||
let mut string_settings = Dictionary::new();
|
|
||||||
string_settings.insert(
|
|
||||||
"foreground".to_string(),
|
|
||||||
Value::String(string_color.to_string()),
|
|
||||||
);
|
|
||||||
string_scope_dict.insert("settings".to_string(), Value::Dictionary(string_settings));
|
|
||||||
settings_array.push(Value::Dictionary(string_scope_dict));
|
|
||||||
|
|
||||||
let mut keyword_scope_dict = Dictionary::new();
|
|
||||||
keyword_scope_dict.insert("name".to_string(), Value::String("Keyword".to_string()));
|
|
||||||
keyword_scope_dict.insert("scope".to_string(), Value::String("keyword".to_string()));
|
|
||||||
let mut keyword_settings = Dictionary::new();
|
|
||||||
keyword_settings.insert(
|
|
||||||
"foreground".to_string(),
|
|
||||||
Value::String(keyword_color.to_string()),
|
|
||||||
);
|
|
||||||
keyword_scope_dict.insert("settings".to_string(), Value::Dictionary(keyword_settings));
|
|
||||||
settings_array.push(Value::Dictionary(keyword_scope_dict));
|
|
||||||
|
|
||||||
root_dict.insert("settings".to_string(), Value::Array(settings_array));
|
|
||||||
|
|
||||||
Value::Dictionary(root_dict)
|
|
||||||
}
|
|
||||||
|
|||||||
192
src/io.rs
192
src/io.rs
@ -7,116 +7,6 @@ pub(crate) fn new_file(app: &mut TextEditor) {
|
|||||||
app.add_new_tab();
|
app.add_new_tab();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_text_file(path: &PathBuf) -> bool {
|
|
||||||
if let Some(extension) = path.extension().and_then(|s| s.to_str()) {
|
|
||||||
matches!(
|
|
||||||
extension.to_lowercase().as_str(),
|
|
||||||
"txt"
|
|
||||||
| "md"
|
|
||||||
| "markdown"
|
|
||||||
| "rs"
|
|
||||||
| "py"
|
|
||||||
| "js"
|
|
||||||
| "ts"
|
|
||||||
| "tsx"
|
|
||||||
| "jsx"
|
|
||||||
| "c"
|
|
||||||
| "cpp"
|
|
||||||
| "cc"
|
|
||||||
| "cxx"
|
|
||||||
| "h"
|
|
||||||
| "hpp"
|
|
||||||
| "java"
|
|
||||||
| "go"
|
|
||||||
| "php"
|
|
||||||
| "rb"
|
|
||||||
| "cs"
|
|
||||||
| "swift"
|
|
||||||
| "kt"
|
|
||||||
| "scala"
|
|
||||||
| "sh"
|
|
||||||
| "bash"
|
|
||||||
| "zsh"
|
|
||||||
| "fish"
|
|
||||||
| "html"
|
|
||||||
| "htm"
|
|
||||||
| "xml"
|
|
||||||
| "css"
|
|
||||||
| "scss"
|
|
||||||
| "sass"
|
|
||||||
| "json"
|
|
||||||
| "yaml"
|
|
||||||
| "yml"
|
|
||||||
| "toml"
|
|
||||||
| "sql"
|
|
||||||
| "lua"
|
|
||||||
| "vim"
|
|
||||||
| "dockerfile"
|
|
||||||
| "makefile"
|
|
||||||
| "gitignore"
|
|
||||||
| "conf"
|
|
||||||
| "cfg"
|
|
||||||
| "ini"
|
|
||||||
| "log"
|
|
||||||
| "csv"
|
|
||||||
| "tsv"
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Files without extensions might be text files, but let's be conservative
|
|
||||||
// and only include them if they're small and readable
|
|
||||||
if let Ok(metadata) = fs::metadata(path) {
|
|
||||||
metadata.len() < 1024 * 1024 // Only consider files smaller than 1MB
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn open_files_from_directory(
|
|
||||||
app: &mut TextEditor,
|
|
||||||
dir_path: PathBuf,
|
|
||||||
) -> Result<usize, String> {
|
|
||||||
if !dir_path.is_dir() {
|
|
||||||
return Err(format!("{} is not a directory", dir_path.display()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let entries = fs::read_dir(&dir_path)
|
|
||||||
.map_err(|e| format!("Failed to read directory {}: {}", dir_path.display(), e))?;
|
|
||||||
|
|
||||||
let mut opened_count = 0;
|
|
||||||
let mut text_files: Vec<PathBuf> = Vec::new();
|
|
||||||
|
|
||||||
// Collect all text files in the directory
|
|
||||||
for entry in entries {
|
|
||||||
let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
|
|
||||||
let path = entry.path();
|
|
||||||
|
|
||||||
if path.is_file() && is_text_file(&path) {
|
|
||||||
text_files.push(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort files by name for consistent ordering
|
|
||||||
text_files.sort();
|
|
||||||
|
|
||||||
// Open each text file
|
|
||||||
for file_path in text_files {
|
|
||||||
match open_file_from_path(app, file_path.clone()) {
|
|
||||||
Ok(()) => opened_count += 1,
|
|
||||||
Err(e) => eprintln!("Warning: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if opened_count == 0 {
|
|
||||||
Err(format!(
|
|
||||||
"No text files found in directory {}",
|
|
||||||
dir_path.display()
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Ok(opened_count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn open_file(app: &mut TextEditor) {
|
pub(crate) fn open_file(app: &mut TextEditor) {
|
||||||
if let Some(path) = rfd::FileDialog::new()
|
if let Some(path) = rfd::FileDialog::new()
|
||||||
.add_filter("Text files", &["*"])
|
.add_filter("Text files", &["*"])
|
||||||
@ -124,6 +14,7 @@ pub(crate) fn open_file(app: &mut TextEditor) {
|
|||||||
{
|
{
|
||||||
match fs::read_to_string(&path) {
|
match fs::read_to_string(&path) {
|
||||||
Ok(content) => {
|
Ok(content) => {
|
||||||
|
// Check if the current active tab is empty/clean and can be replaced
|
||||||
let should_replace_current_tab = if let Some(active_tab) = app.get_active_tab() {
|
let should_replace_current_tab = if let Some(active_tab) = app.get_active_tab() {
|
||||||
active_tab.file_path.is_none()
|
active_tab.file_path.is_none()
|
||||||
&& active_tab.content.is_empty()
|
&& active_tab.content.is_empty()
|
||||||
@ -133,29 +24,22 @@ pub(crate) fn open_file(app: &mut TextEditor) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if should_replace_current_tab {
|
if should_replace_current_tab {
|
||||||
|
// Replace the current empty tab
|
||||||
if let Some(active_tab) = app.get_active_tab_mut() {
|
if let Some(active_tab) = app.get_active_tab_mut() {
|
||||||
let title = path
|
active_tab.content = content;
|
||||||
|
active_tab.file_path = Some(path.clone());
|
||||||
|
active_tab.title = path
|
||||||
.file_name()
|
.file_name()
|
||||||
.and_then(|n| n.to_str())
|
.and_then(|n| n.to_str())
|
||||||
.unwrap_or("Untitled");
|
.unwrap_or("Untitled")
|
||||||
active_tab.content = content;
|
.to_string();
|
||||||
active_tab.file_path = Some(path.to_path_buf());
|
active_tab.mark_as_saved(); // This will set the hash and mark as not modified
|
||||||
active_tab.title = title.to_string();
|
|
||||||
active_tab.mark_as_saved();
|
|
||||||
}
|
}
|
||||||
app.text_needs_processing = true;
|
|
||||||
} else {
|
} else {
|
||||||
|
// Create a new tab as before
|
||||||
let new_tab = Tab::new_with_file(content, path);
|
let new_tab = Tab::new_with_file(content, path);
|
||||||
app.tabs.push(new_tab);
|
app.tabs.push(new_tab);
|
||||||
app.active_tab_index = app.tabs.len() - 1;
|
app.active_tab_index = app.tabs.len() - 1;
|
||||||
app.text_needs_processing = true;
|
|
||||||
}
|
|
||||||
if app.show_find && !app.find_query.is_empty() {
|
|
||||||
app.update_find_matches();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = app.save_state_cache() {
|
|
||||||
eprintln!("Failed to save state cache: {e}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@ -165,54 +49,10 @@ pub(crate) fn open_file(app: &mut TextEditor) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn open_file_from_path(app: &mut TextEditor, path: PathBuf) -> Result<(), String> {
|
|
||||||
match fs::read_to_string(&path) {
|
|
||||||
Ok(content) => {
|
|
||||||
let should_replace_current_tab = if let Some(active_tab) = app.get_active_tab() {
|
|
||||||
active_tab.file_path.is_none()
|
|
||||||
&& active_tab.content.is_empty()
|
|
||||||
&& !active_tab.is_modified
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
if should_replace_current_tab {
|
|
||||||
if let Some(active_tab) = app.get_active_tab_mut() {
|
|
||||||
let title = path
|
|
||||||
.file_name()
|
|
||||||
.and_then(|n| n.to_str())
|
|
||||||
.unwrap_or("Untitled");
|
|
||||||
active_tab.content = content;
|
|
||||||
active_tab.file_path = Some(path.to_path_buf());
|
|
||||||
active_tab.title = title.to_string();
|
|
||||||
active_tab.mark_as_saved();
|
|
||||||
}
|
|
||||||
app.text_needs_processing = true;
|
|
||||||
} else {
|
|
||||||
let new_tab = Tab::new_with_file(content, path);
|
|
||||||
app.tabs.push(new_tab);
|
|
||||||
app.active_tab_index = app.tabs.len() - 1;
|
|
||||||
app.text_needs_processing = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if app.show_find && !app.find_query.is_empty() {
|
|
||||||
app.update_find_matches();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = app.save_state_cache() {
|
|
||||||
eprintln!("Failed to save state cache: {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(err) => Err(format!("Failed to open file {}: {}", path.display(), err)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn save_file(app: &mut TextEditor) {
|
pub(crate) fn save_file(app: &mut TextEditor) {
|
||||||
if let Some(active_tab) = app.get_active_tab() {
|
if let Some(active_tab) = app.get_active_tab() {
|
||||||
if let Some(path) = &active_tab.file_path {
|
if let Some(path) = &active_tab.file_path {
|
||||||
save_to_path(app, path.to_path_buf());
|
save_to_path(app, path.clone());
|
||||||
} else {
|
} else {
|
||||||
save_as_file(app);
|
save_as_file(app);
|
||||||
}
|
}
|
||||||
@ -232,17 +72,13 @@ pub(crate) fn save_to_path(app: &mut TextEditor, path: PathBuf) {
|
|||||||
if let Some(active_tab) = app.get_active_tab_mut() {
|
if let Some(active_tab) = app.get_active_tab_mut() {
|
||||||
match fs::write(&path, &active_tab.content) {
|
match fs::write(&path, &active_tab.content) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let title = path
|
active_tab.file_path = Some(path.clone());
|
||||||
|
active_tab.title = path
|
||||||
.file_name()
|
.file_name()
|
||||||
.and_then(|n| n.to_str())
|
.and_then(|n| n.to_str())
|
||||||
.unwrap_or("Untitled");
|
.unwrap_or("Untitled")
|
||||||
active_tab.file_path = Some(path.to_path_buf());
|
.to_string();
|
||||||
active_tab.title = title.to_string();
|
|
||||||
active_tab.mark_as_saved();
|
active_tab.mark_as_saved();
|
||||||
|
|
||||||
if let Err(e) = app.save_state_cache() {
|
|
||||||
eprintln!("Failed to save state cache: {e}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Failed to save file: {err}");
|
eprintln!("Failed to save file: {err}");
|
||||||
|
|||||||
19
src/main.rs
19
src/main.rs
@ -1,9 +1,6 @@
|
|||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use std::env;
|
|
||||||
use std::io::IsTerminal;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
mod io;
|
mod io;
|
||||||
@ -11,14 +8,6 @@ mod ui;
|
|||||||
use app::{config::Config, TextEditor};
|
use app::{config::Config, TextEditor};
|
||||||
|
|
||||||
fn main() -> eframe::Result {
|
fn main() -> eframe::Result {
|
||||||
let args: Vec<String> = env::args().collect();
|
|
||||||
|
|
||||||
let initial_paths: Vec<PathBuf> = args.iter().skip(1).map(|arg| PathBuf::from(arg)).collect();
|
|
||||||
|
|
||||||
if std::io::stdin().is_terminal() {
|
|
||||||
println!("This is a GUI application, are you sure you want to launch from terminal?");
|
|
||||||
}
|
|
||||||
|
|
||||||
let options = eframe::NativeOptions {
|
let options = eframe::NativeOptions {
|
||||||
viewport: egui::ViewportBuilder::default()
|
viewport: egui::ViewportBuilder::default()
|
||||||
.with_min_inner_size([600.0, 400.0])
|
.with_min_inner_size([600.0, 400.0])
|
||||||
@ -32,12 +21,6 @@ fn main() -> eframe::Result {
|
|||||||
eframe::run_native(
|
eframe::run_native(
|
||||||
"ced",
|
"ced",
|
||||||
options,
|
options,
|
||||||
Box::new(move |cc| {
|
Box::new(move |cc| Ok(Box::new(TextEditor::from_config_with_context(config, cc)))),
|
||||||
Ok(Box::new(TextEditor::from_config_with_context(
|
|
||||||
config,
|
|
||||||
cc,
|
|
||||||
initial_paths,
|
|
||||||
)))
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
pub(crate) mod about_window;
|
pub(crate) mod about_window;
|
||||||
pub(crate) mod central_panel;
|
pub(crate) mod central_panel;
|
||||||
pub(crate) mod constants;
|
|
||||||
pub(crate) mod find_window;
|
pub(crate) mod find_window;
|
||||||
pub(crate) mod menu_bar;
|
pub(crate) mod menu_bar;
|
||||||
pub(crate) mod preferences_window;
|
pub(crate) mod preferences_window;
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
use crate::app::TextEditor;
|
use crate::app::TextEditor;
|
||||||
use crate::ui::constants::*;
|
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
|
|
||||||
pub(crate) fn about_window(app: &mut TextEditor, ctx: &egui::Context) {
|
pub(crate) fn about_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||||
@ -12,25 +11,23 @@ pub(crate) fn about_window(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
.collapsible(false)
|
.collapsible(false)
|
||||||
.resizable(false)
|
.resizable(false)
|
||||||
.anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
|
.anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
|
||||||
.fade_in(true)
|
|
||||||
.fade_out(true)
|
|
||||||
.frame(egui::Frame {
|
.frame(egui::Frame {
|
||||||
fill: visuals.window_fill,
|
fill: visuals.window_fill,
|
||||||
stroke: visuals.window_stroke,
|
stroke: visuals.window_stroke,
|
||||||
corner_radius: egui::CornerRadius::same(CORNER_RADIUS),
|
corner_radius: egui::CornerRadius::same(8),
|
||||||
shadow: visuals.window_shadow,
|
shadow: visuals.window_shadow,
|
||||||
inner_margin: egui::Margin::same(INNER_MARGIN),
|
inner_margin: egui::Margin::same(16),
|
||||||
outer_margin: egui::Margin::same(0),
|
outer_margin: egui::Margin::same(0),
|
||||||
})
|
})
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
ui.label(
|
ui.label(
|
||||||
egui::RichText::new("A stupidly simple, responsive text editor.")
|
egui::RichText::new("A stupidly simple, responsive text editor.")
|
||||||
.size(UI_TEXT_SIZE)
|
.size(14.0)
|
||||||
.weak(),
|
.weak(),
|
||||||
);
|
);
|
||||||
|
|
||||||
ui.add_space(LARGE);
|
ui.add_space(12.0);
|
||||||
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)
|
||||||
|
|||||||
@ -1,12 +1,9 @@
|
|||||||
mod editor;
|
mod editor;
|
||||||
mod find_highlight;
|
mod find_highlight;
|
||||||
mod languages;
|
|
||||||
mod line_numbers;
|
mod line_numbers;
|
||||||
|
|
||||||
use crate::app::TextEditor;
|
use crate::app::TextEditor;
|
||||||
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::line_numbers::{get_visual_line_mapping, render_line_numbers};
|
use self::line_numbers::{get_visual_line_mapping, render_line_numbers};
|
||||||
@ -38,7 +35,7 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
editor_view_ui(ui, app);
|
editor_view_ui(ui, app);
|
||||||
});
|
});
|
||||||
|
|
||||||
handle_empty(ui, app, &context_response);
|
show_context_menu(ui, app, &context_response);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -75,35 +72,39 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let separator_widget = |ui: &mut egui::Ui| {
|
let separator_widget = |ui: &mut egui::Ui| {
|
||||||
ui.add_space(SMALL);
|
ui.add_space(3.0);
|
||||||
let separator_x = ui.cursor().left();
|
let separator_x = ui.cursor().left();
|
||||||
let mut y_range = ui.available_rect_before_wrap().y_range();
|
let mut y_range = ui.available_rect_before_wrap().y_range();
|
||||||
y_range.max += 2.0 * font_size;
|
y_range.max += 2.0 * font_size; // Extend separator to cover more vertical space
|
||||||
ui.painter()
|
ui.painter()
|
||||||
.vline(separator_x, y_range, ui.visuals().window_stroke);
|
.vline(separator_x, y_range, ui.visuals().window_stroke);
|
||||||
ui.add_space(SMALL);
|
ui.add_space(4.0);
|
||||||
};
|
};
|
||||||
|
|
||||||
egui::ScrollArea::vertical()
|
egui::ScrollArea::vertical()
|
||||||
.auto_shrink([false; 2])
|
.auto_shrink([false; 2])
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
if line_side {
|
if line_side {
|
||||||
|
// Line numbers on the right
|
||||||
let text_editor_width =
|
let text_editor_width =
|
||||||
editor_dimensions.text_width + editor_dimensions.total_reserved_width;
|
editor_dimensions.text_width + editor_dimensions.total_reserved_width;
|
||||||
ui.allocate_ui_with_layout(
|
ui.allocate_ui_with_layout(
|
||||||
egui::vec2(text_editor_width, editor_height),
|
egui::vec2(text_editor_width, editor_height),
|
||||||
egui::Layout::left_to_right(egui::Align::TOP),
|
egui::Layout::left_to_right(egui::Align::TOP),
|
||||||
|ui| {
|
|ui| {
|
||||||
|
// Constrain editor to specific width to leave space for line numbers
|
||||||
ui.allocate_ui_with_layout(
|
ui.allocate_ui_with_layout(
|
||||||
egui::vec2(editor_dimensions.text_width, editor_height),
|
egui::vec2(editor_dimensions.text_width, editor_height),
|
||||||
egui::Layout::left_to_right(egui::Align::TOP),
|
egui::Layout::left_to_right(egui::Align::TOP),
|
||||||
|ui| {
|
|ui| {
|
||||||
|
// Create an invisible interaction area for context menu
|
||||||
let full_rect = ui.available_rect_before_wrap();
|
let full_rect = ui.available_rect_before_wrap();
|
||||||
let context_response = ui.allocate_response(
|
let context_response = ui.allocate_response(
|
||||||
full_rect.size(),
|
full_rect.size(),
|
||||||
egui::Sense::click(),
|
egui::Sense::click(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Reset cursor to render editor at the top
|
||||||
ui.scope_builder(
|
ui.scope_builder(
|
||||||
egui::UiBuilder::new().max_rect(full_rect),
|
egui::UiBuilder::new().max_rect(full_rect),
|
||||||
|ui| {
|
|ui| {
|
||||||
@ -111,7 +112,7 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
handle_empty(ui, app, &context_response);
|
show_context_menu(ui, app, &context_response);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
separator_widget(ui);
|
separator_widget(ui);
|
||||||
@ -119,6 +120,7 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
// Line numbers on the left
|
||||||
let text_editor_width =
|
let text_editor_width =
|
||||||
editor_dimensions.text_width + editor_dimensions.total_reserved_width;
|
editor_dimensions.text_width + editor_dimensions.total_reserved_width;
|
||||||
ui.allocate_ui_with_layout(
|
ui.allocate_ui_with_layout(
|
||||||
@ -128,10 +130,12 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
line_numbers_widget(ui);
|
line_numbers_widget(ui);
|
||||||
separator_widget(ui);
|
separator_widget(ui);
|
||||||
|
|
||||||
|
// Create an invisible interaction area for context menu
|
||||||
let editor_area = ui.available_rect_before_wrap();
|
let editor_area = ui.available_rect_before_wrap();
|
||||||
let context_response =
|
let context_response =
|
||||||
ui.allocate_response(editor_area.size(), egui::Sense::click());
|
ui.allocate_response(editor_area.size(), egui::Sense::click());
|
||||||
|
|
||||||
|
// Reset cursor to render editor at the current position
|
||||||
ui.scope_builder(
|
ui.scope_builder(
|
||||||
egui::UiBuilder::new().max_rect(editor_area),
|
egui::UiBuilder::new().max_rect(editor_area),
|
||||||
|ui| {
|
|ui| {
|
||||||
@ -139,7 +143,7 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
handle_empty(ui, app, &context_response);
|
show_context_menu(ui, app, &context_response);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -147,25 +151,7 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_empty(_ui: &mut egui::Ui, app: &mut TextEditor, context_response: &egui::Response) {
|
fn show_context_menu(_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| {
|
context_response.context_menu(|ui| {
|
||||||
let text_len = app.get_active_tab().unwrap().content.len();
|
let text_len = app.get_active_tab().unwrap().content.len();
|
||||||
let reset_zoom_key = egui::Id::new("editor_reset_zoom");
|
let reset_zoom_key = egui::Id::new("editor_reset_zoom");
|
||||||
@ -173,17 +159,17 @@ fn handle_empty(_ui: &mut egui::Ui, app: &mut TextEditor, context_response: &egu
|
|||||||
if ui.button("Cut").clicked() {
|
if ui.button("Cut").clicked() {
|
||||||
ui.ctx()
|
ui.ctx()
|
||||||
.send_viewport_cmd(egui::ViewportCommand::RequestCut);
|
.send_viewport_cmd(egui::ViewportCommand::RequestCut);
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
if ui.button("Copy").clicked() {
|
if ui.button("Copy").clicked() {
|
||||||
ui.ctx()
|
ui.ctx()
|
||||||
.send_viewport_cmd(egui::ViewportCommand::RequestCopy);
|
.send_viewport_cmd(egui::ViewportCommand::RequestCopy);
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
if ui.button("Paste").clicked() {
|
if ui.button("Paste").clicked() {
|
||||||
ui.ctx()
|
ui.ctx()
|
||||||
.send_viewport_cmd(egui::ViewportCommand::RequestPaste);
|
.send_viewport_cmd(egui::ViewportCommand::RequestPaste);
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
if ui.button("Delete").clicked() {
|
if ui.button("Delete").clicked() {
|
||||||
ui.ctx().input_mut(|i| {
|
ui.ctx().input_mut(|i| {
|
||||||
@ -195,7 +181,7 @@ fn handle_empty(_ui: &mut egui::Ui, app: &mut TextEditor, context_response: &egu
|
|||||||
modifiers: egui::Modifiers::NONE,
|
modifiers: egui::Modifiers::NONE,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
if ui.button("Select All").clicked() {
|
if ui.button("Select All").clicked() {
|
||||||
let text_edit_id = egui::Id::new("main_text_editor");
|
let text_edit_id = egui::Id::new("main_text_editor");
|
||||||
@ -207,14 +193,14 @@ fn handle_empty(_ui: &mut egui::Ui, app: &mut TextEditor, context_response: &egu
|
|||||||
state.cursor.set_char_range(Some(select_all_range));
|
state.cursor.set_char_range(Some(select_all_range));
|
||||||
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_menu();
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if ui.button("Reset Zoom").clicked() {
|
if ui.button("Reset Zoom").clicked() {
|
||||||
ui.ctx().memory_mut(|mem| {
|
ui.ctx().memory_mut(|mem| {
|
||||||
mem.data.insert_temp(reset_zoom_key, true);
|
mem.data.insert_temp(reset_zoom_key, true);
|
||||||
});
|
});
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
use crate::app::TextEditor;
|
use crate::app::TextEditor;
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use egui_extras::syntax_highlighting::{self};
|
|
||||||
|
|
||||||
use super::find_highlight;
|
|
||||||
|
|
||||||
pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::Response {
|
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 _current_match_position = app.get_current_match_position();
|
||||||
@ -13,8 +10,6 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
|||||||
let show_shortcuts = app.show_shortcuts;
|
let show_shortcuts = app.show_shortcuts;
|
||||||
let word_wrap = app.word_wrap;
|
let word_wrap = app.word_wrap;
|
||||||
let font_size = app.font_size;
|
let font_size = app.font_size;
|
||||||
let font_id = app.get_font_id();
|
|
||||||
let syntax_highlighting_enabled = app.syntax_highlighting;
|
|
||||||
|
|
||||||
let reset_zoom_key = egui::Id::new("editor_reset_zoom");
|
let reset_zoom_key = egui::Id::new("editor_reset_zoom");
|
||||||
let should_reset_zoom = ui
|
let should_reset_zoom = ui
|
||||||
@ -35,18 +30,6 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
|||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
|
|
||||||
let find_data = if show_find && !app.find_matches.is_empty() {
|
|
||||||
app.get_active_tab().map(|tab| {
|
|
||||||
(
|
|
||||||
tab.content.to_owned(),
|
|
||||||
app.find_matches.to_owned(),
|
|
||||||
app.current_match_index,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(active_tab) = app.get_active_tab_mut() else {
|
let Some(active_tab) = app.get_active_tab_mut() else {
|
||||||
return ui.label("No file open, how did you get here?");
|
return ui.label("No file open, how did you get here?");
|
||||||
};
|
};
|
||||||
@ -55,83 +38,20 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
|||||||
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);
|
||||||
|
|
||||||
if let Some((content, matches, current_match_index)) = &find_data {
|
|
||||||
let font_id = ui
|
|
||||||
.style()
|
|
||||||
.text_styles
|
|
||||||
.get(&egui::TextStyle::Monospace)
|
|
||||||
.unwrap_or(&egui::FontId::monospace(font_size))
|
|
||||||
.to_owned();
|
|
||||||
|
|
||||||
let desired_width = if word_wrap {
|
|
||||||
ui.available_width()
|
|
||||||
} else {
|
|
||||||
f32::INFINITY
|
|
||||||
};
|
|
||||||
|
|
||||||
let temp_galley = ui.fonts(|fonts| {
|
|
||||||
fonts.layout(
|
|
||||||
content.to_owned(),
|
|
||||||
font_id.to_owned(),
|
|
||||||
ui.visuals().text_color(),
|
|
||||||
desired_width,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let text_area_left = editor_rect.left() + 4.0;
|
|
||||||
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 desired_width = if word_wrap {
|
let desired_width = if word_wrap {
|
||||||
ui.available_width()
|
ui.available_width()
|
||||||
} else {
|
} else {
|
||||||
f32::INFINITY
|
f32::INFINITY
|
||||||
};
|
};
|
||||||
|
|
||||||
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 syntect_theme =
|
|
||||||
// crate::app::theme::create_code_theme_from_visuals(ui.visuals(), font_size);
|
|
||||||
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" {
|
|
||||||
// let mut settings = egui_extras::syntax_highlighting::SyntectSettings::default();
|
|
||||||
// settings.ts = syntect_theme;
|
|
||||||
// syntax_highlighting::highlight_with(ui.ctx(), &ui.style().clone(), &theme, text, &language, &settings)
|
|
||||||
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.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layout_job.wrap.max_width = wrap_width;
|
|
||||||
ui.fonts(|f| f.layout_job(layout_job))
|
|
||||||
};
|
|
||||||
|
|
||||||
let text_edit = egui::TextEdit::multiline(&mut active_tab.content)
|
let text_edit = egui::TextEdit::multiline(&mut active_tab.content)
|
||||||
.frame(false)
|
.frame(false)
|
||||||
|
.font(egui::TextStyle::Monospace)
|
||||||
.code_editor()
|
.code_editor()
|
||||||
.desired_width(desired_width)
|
.desired_width(desired_width)
|
||||||
.desired_rows(0)
|
.desired_rows(0)
|
||||||
.lock_focus(!show_find)
|
.lock_focus(true)
|
||||||
.cursor_at_end(false)
|
.cursor_at_end(false)
|
||||||
.layouter(&mut layouter)
|
|
||||||
.id(egui::Id::new("main_text_editor"));
|
.id(egui::Id::new("main_text_editor"));
|
||||||
|
|
||||||
let output = if word_wrap {
|
let output = if word_wrap {
|
||||||
@ -153,21 +73,11 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
|||||||
let content_changed = output.response.changed();
|
let content_changed = output.response.changed();
|
||||||
let content_for_processing = if content_changed {
|
let content_for_processing = if content_changed {
|
||||||
active_tab.update_modified_state();
|
active_tab.update_modified_state();
|
||||||
Some(active_tab.content.to_owned())
|
Some(active_tab.content.clone())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
if content_changed {
|
|
||||||
if let Err(e) = app.save_state_cache() {
|
|
||||||
eprintln!("Failed to save state cache: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if content_changed && app.show_find && !app.find_query.is_empty() {
|
|
||||||
app.update_find_matches();
|
|
||||||
}
|
|
||||||
|
|
||||||
let current_cursor_pos = output
|
let current_cursor_pos = output
|
||||||
.state
|
.state
|
||||||
.cursor
|
.cursor
|
||||||
@ -175,7 +85,7 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
|||||||
.map(|range| range.primary.index);
|
.map(|range| range.primary.index);
|
||||||
|
|
||||||
if let Some(content) = content_for_processing {
|
if let Some(content) = content_for_processing {
|
||||||
let previous_content = app.previous_content.to_owned();
|
let previous_content = app.previous_content.clone();
|
||||||
let previous_cursor_pos = app.previous_cursor_char_index;
|
let previous_cursor_pos = app.previous_cursor_char_index;
|
||||||
|
|
||||||
if !previous_content.is_empty() {
|
if !previous_content.is_empty() {
|
||||||
@ -189,22 +99,36 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
|||||||
curr_cursor_pos,
|
curr_cursor_pos,
|
||||||
ui,
|
ui,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
app.process_text_for_rendering(&content, ui);
|
||||||
|
|
||||||
|
if let Some(cursor_pos) = current_cursor_pos {
|
||||||
|
app.current_cursor_line = content[..cursor_pos]
|
||||||
|
.bytes()
|
||||||
|
.filter(|&b| b == b'\n')
|
||||||
|
.count();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
app.process_text_for_rendering(&content, ui);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app.previous_content = content.to_owned();
|
app.previous_content = content.clone();
|
||||||
app.previous_cursor_char_index = current_cursor_pos;
|
app.previous_cursor_char_index = current_cursor_pos;
|
||||||
|
|
||||||
|
if let Some(active_tab) = app.get_active_tab_mut() {
|
||||||
|
active_tab.last_content_hash =
|
||||||
|
crate::app::tab::compute_content_hash(&active_tab.content, &mut active_tab.hasher);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if app.font_settings_changed || app.text_needs_processing {
|
// Check if font settings changed and trigger reprocessing
|
||||||
|
if app.font_settings_changed {
|
||||||
if let Some(active_tab) = app.get_active_tab() {
|
if let Some(active_tab) = app.get_active_tab() {
|
||||||
let content = active_tab.content.to_owned();
|
let content = active_tab.content.clone();
|
||||||
app.process_text_for_rendering(&content, ui);
|
if !content.is_empty() {
|
||||||
|
app.process_text_for_rendering(&content, ui);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
app.font_settings_changed = false;
|
app.font_settings_changed = false;
|
||||||
app.text_needs_processing = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !word_wrap {
|
if !word_wrap {
|
||||||
@ -226,7 +150,7 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
|||||||
.text_styles
|
.text_styles
|
||||||
.get(&egui::TextStyle::Monospace)
|
.get(&egui::TextStyle::Monospace)
|
||||||
.unwrap_or(&egui::FontId::monospace(font_size))
|
.unwrap_or(&egui::FontId::monospace(font_size))
|
||||||
.to_owned();
|
.clone();
|
||||||
let line_height = ui.fonts(|fonts| fonts.row_height(&font_id));
|
let line_height = ui.fonts(|fonts| fonts.row_height(&font_id));
|
||||||
|
|
||||||
let y_pos = output.response.rect.top() + (cursor_line as f32 * line_height);
|
let y_pos = output.response.rect.top() + (cursor_line as f32 * line_height);
|
||||||
@ -241,10 +165,12 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app.previous_cursor_position = Some(cursor_pos);
|
app.previous_cursor_position = Some(cursor_pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Request focus if no dialogs are open
|
||||||
if !output.response.has_focus()
|
if !output.response.has_focus()
|
||||||
&& !show_preferences
|
&& !show_preferences
|
||||||
&& !show_about
|
&& !show_about
|
||||||
|
|||||||
@ -1,23 +1,11 @@
|
|||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
|
|
||||||
/// Safely get a string slice up to a byte position, ensuring UTF-8 boundaries
|
pub(super) fn _draw_find_highlight(
|
||||||
fn safe_slice_to_pos(content: &str, pos: usize) -> &str {
|
|
||||||
let pos = pos.min(content.len());
|
|
||||||
let mut boundary_pos = pos;
|
|
||||||
while boundary_pos > 0 && !content.is_char_boundary(boundary_pos) {
|
|
||||||
boundary_pos -= 1;
|
|
||||||
}
|
|
||||||
&content[..boundary_pos]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn draw_find_highlights(
|
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
content: &str,
|
content: &str,
|
||||||
matches: &[(usize, usize)],
|
start_pos: usize,
|
||||||
current_match_index: Option<usize>,
|
end_pos: usize,
|
||||||
galley: &std::sync::Arc<egui::Galley>,
|
editor_rect: egui::Rect,
|
||||||
text_area_left: f32,
|
|
||||||
text_area_top: f32,
|
|
||||||
font_size: f32,
|
font_size: f32,
|
||||||
) {
|
) {
|
||||||
let font_id = ui
|
let font_id = ui
|
||||||
@ -25,47 +13,15 @@ pub(super) fn draw_find_highlights(
|
|||||||
.text_styles
|
.text_styles
|
||||||
.get(&egui::TextStyle::Monospace)
|
.get(&egui::TextStyle::Monospace)
|
||||||
.unwrap_or(&egui::FontId::monospace(font_size))
|
.unwrap_or(&egui::FontId::monospace(font_size))
|
||||||
.to_owned();
|
.clone();
|
||||||
|
|
||||||
for (match_index, &(start_pos, end_pos)) in matches.iter().enumerate() {
|
let text_up_to_start = &content[..start_pos.min(content.len())];
|
||||||
let is_current_match = current_match_index == Some(match_index);
|
|
||||||
draw_single_highlight(
|
|
||||||
ui,
|
|
||||||
content,
|
|
||||||
start_pos,
|
|
||||||
end_pos,
|
|
||||||
text_area_left,
|
|
||||||
text_area_top,
|
|
||||||
galley,
|
|
||||||
&font_id,
|
|
||||||
is_current_match,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_single_highlight(
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
content: &str,
|
|
||||||
start_pos: usize,
|
|
||||||
end_pos: usize,
|
|
||||||
text_area_left: f32,
|
|
||||||
text_area_top: f32,
|
|
||||||
galley: &std::sync::Arc<egui::Galley>,
|
|
||||||
font_id: &egui::FontId,
|
|
||||||
is_current_match: bool,
|
|
||||||
) {
|
|
||||||
let text_up_to_start = safe_slice_to_pos(content, start_pos);
|
|
||||||
let start_line = text_up_to_start.chars().filter(|&c| c == '\n').count();
|
let start_line = text_up_to_start.chars().filter(|&c| c == '\n').count();
|
||||||
|
|
||||||
if start_line >= galley.rows.len() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let line_start_byte_pos = text_up_to_start.rfind('\n').map(|pos| pos + 1).unwrap_or(0);
|
let line_start_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)
|
let line_start_char_pos = content[..line_start_byte_pos].chars().count();
|
||||||
.chars()
|
let start_char_pos = content[..start_pos].chars().count();
|
||||||
.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 start_col = start_char_pos - line_start_char_pos;
|
||||||
|
|
||||||
let lines: Vec<&str> = content.lines().collect();
|
let lines: Vec<&str> = content.lines().collect();
|
||||||
@ -76,11 +32,18 @@ fn draw_single_highlight(
|
|||||||
let line_text = lines[start_line];
|
let line_text = lines[start_line];
|
||||||
let text_before_match: String = line_text.chars().take(start_col).collect();
|
let text_before_match: String = line_text.chars().take(start_col).collect();
|
||||||
|
|
||||||
|
let line_height = ui.fonts(|fonts| fonts.row_height(&font_id));
|
||||||
|
|
||||||
|
let horizontal_margin = ui.spacing().button_padding.x - 4.0;
|
||||||
|
let vertical_margin = ui.spacing().button_padding.y - 1.0;
|
||||||
|
let text_area_left = editor_rect.left() + horizontal_margin;
|
||||||
|
let text_area_top = editor_rect.top() + vertical_margin;
|
||||||
|
|
||||||
let text_before_width = ui.fonts(|fonts| {
|
let text_before_width = ui.fonts(|fonts| {
|
||||||
fonts
|
fonts
|
||||||
.layout(
|
.layout(
|
||||||
text_before_match,
|
text_before_match,
|
||||||
font_id.to_owned(),
|
font_id.clone(),
|
||||||
egui::Color32::WHITE,
|
egui::Color32::WHITE,
|
||||||
f32::INFINITY,
|
f32::INFINITY,
|
||||||
)
|
)
|
||||||
@ -88,35 +51,30 @@ fn draw_single_highlight(
|
|||||||
.x
|
.x
|
||||||
});
|
});
|
||||||
|
|
||||||
let galley_row = &galley.rows[start_line];
|
let start_y = text_area_top + (start_line as f32 * line_height);
|
||||||
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 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| {
|
let match_text = &content[start_pos..end_pos.min(content.len())];
|
||||||
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(
|
let match_width = ui.fonts(|fonts| {
|
||||||
egui::pos2(start_x, start_y),
|
fonts
|
||||||
egui::vec2(match_width, line_height),
|
.layout(
|
||||||
);
|
match_text.to_string(),
|
||||||
|
font_id.clone(),
|
||||||
|
ui.visuals().text_color(),
|
||||||
|
f32::INFINITY,
|
||||||
|
)
|
||||||
|
.size()
|
||||||
|
.x
|
||||||
|
});
|
||||||
|
|
||||||
let highlight_color = if is_current_match {
|
let highlight_rect = egui::Rect::from_min_size(
|
||||||
ui.visuals().selection.bg_fill
|
egui::pos2(start_x, start_y),
|
||||||
} else {
|
egui::vec2(match_width, line_height),
|
||||||
ui.visuals().selection.bg_fill.gamma_multiply(0.6)
|
);
|
||||||
};
|
|
||||||
|
|
||||||
let painter = ui.painter();
|
ui.painter()
|
||||||
painter.rect_filled(highlight_rect, 0.0, highlight_color);
|
.rect_filled(highlight_rect, 0.0, ui.visuals().selection.bg_fill);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,55 +0,0 @@
|
|||||||
pub fn get_language_from_extension(file_path: Option<&std::path::Path>) -> String {
|
|
||||||
let default_lang = "txt".to_string();
|
|
||||||
|
|
||||||
let path = match file_path {
|
|
||||||
Some(p) => p,
|
|
||||||
None => return default_lang,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
|
|
||||||
match extension.to_lowercase().as_str() {
|
|
||||||
"rs" => "rs".to_string(),
|
|
||||||
"py" => "py".to_string(),
|
|
||||||
"js" => "js".to_string(),
|
|
||||||
"ts" => "ts".to_string(),
|
|
||||||
"tsx" => "tsx".to_string(),
|
|
||||||
"jsx" => "jsx".to_string(),
|
|
||||||
"c" => "c".to_string(),
|
|
||||||
"cpp" | "cc" | "cxx" => "cpp".to_string(),
|
|
||||||
"h" | "hpp" => "cpp".to_string(),
|
|
||||||
"java" => "java".to_string(),
|
|
||||||
"go" => "go".to_string(),
|
|
||||||
"php" => "php".to_string(),
|
|
||||||
"rb" => "rb".to_string(),
|
|
||||||
"cs" => "cs".to_string(),
|
|
||||||
"swift" => "swift".to_string(),
|
|
||||||
"kt" => "kt".to_string(),
|
|
||||||
"scala" => "scala".to_string(),
|
|
||||||
"sh" | "bash" | "zsh" | "fish" => "sh".to_string(),
|
|
||||||
"html" | "htm" => "html".to_string(),
|
|
||||||
"xml" => "xml".to_string(),
|
|
||||||
"css" => "css".to_string(),
|
|
||||||
"scss" | "sass" => "scss".to_string(),
|
|
||||||
"json" => "json".to_string(),
|
|
||||||
"yaml" | "yml" => "yaml".to_string(),
|
|
||||||
"toml" => "toml".to_string(),
|
|
||||||
"md" | "markdown" => "md".to_string(),
|
|
||||||
"sql" => "sql".to_string(),
|
|
||||||
"lua" => "lua".to_string(),
|
|
||||||
"vim" => "vim".to_string(),
|
|
||||||
"dockerfile" => "dockerfile".to_string(),
|
|
||||||
"makefile" => "makefile".to_string(),
|
|
||||||
_ => default_lang,
|
|
||||||
}
|
|
||||||
} else if let Some(filename) = path.file_name().and_then(|name| name.to_str()) {
|
|
||||||
match filename.to_lowercase().as_str() {
|
|
||||||
"dockerfile" => "dockerfile".to_string(),
|
|
||||||
"makefile" => "makefile".to_string(),
|
|
||||||
"cargo.toml" | "pyproject.toml" => "toml".to_string(),
|
|
||||||
"package.json" => "json".to_string(),
|
|
||||||
_ => default_lang,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
default_lang
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -29,7 +29,7 @@ pub(super) fn get_visual_line_mapping(
|
|||||||
cache
|
cache
|
||||||
.borrow()
|
.borrow()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|(_, _, mapping)| mapping.to_owned())
|
.map(|(_, _, mapping)| mapping.clone())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -52,7 +52,7 @@ fn calculate_visual_line_mapping(
|
|||||||
let galley = ui.fonts(|fonts| {
|
let galley = ui.fonts(|fonts| {
|
||||||
fonts.layout(
|
fonts.layout(
|
||||||
line.to_string(),
|
line.to_string(),
|
||||||
font_id.to_owned(),
|
font_id.clone(),
|
||||||
egui::Color32::WHITE,
|
egui::Color32::WHITE,
|
||||||
available_width,
|
available_width,
|
||||||
)
|
)
|
||||||
@ -100,7 +100,7 @@ pub(super) fn render_line_numbers(
|
|||||||
};
|
};
|
||||||
ui.label(
|
ui.label(
|
||||||
egui::RichText::new(text)
|
egui::RichText::new(text)
|
||||||
.font(font_id.to_owned())
|
.font(font_id.clone())
|
||||||
.color(text_color),
|
.color(text_color),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -109,7 +109,7 @@ pub(super) fn render_line_numbers(
|
|||||||
let text = format!("{:>width$}", i, width = line_count_width);
|
let text = format!("{:>width$}", i, width = line_count_width);
|
||||||
ui.label(
|
ui.label(
|
||||||
egui::RichText::new(text)
|
egui::RichText::new(text)
|
||||||
.font(font_id.to_owned())
|
.font(font_id.clone())
|
||||||
.color(text_color),
|
.color(text_color),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
pub const SMALL: f32 = 4.0;
|
|
||||||
pub const MEDIUM: f32 = 8.0;
|
|
||||||
pub const LARGE: f32 = 12.0;
|
|
||||||
pub const VLARGE: f32 = 16.0;
|
|
||||||
|
|
||||||
pub const UI_HEADER_SIZE: f32 = 18.0;
|
|
||||||
pub const UI_TEXT_SIZE: f32 = 14.0;
|
|
||||||
|
|
||||||
pub const MIN_FONT_SIZE: f32 = 8.0;
|
|
||||||
pub const MAX_FONT_SIZE: f32 = 32.0;
|
|
||||||
|
|
||||||
pub const WINDOW_WIDTH_RATIO: f32 = 0.6;
|
|
||||||
pub const WINDOW_HEIGHT_RATIO: f32 = 0.7;
|
|
||||||
pub const WINDOW_MIN_WIDTH: f32 = 300.0;
|
|
||||||
pub const WINDOW_MAX_WIDTH: f32 = 400.0;
|
|
||||||
pub const WINDOW_MIN_HEIGHT: f32 = 250.0;
|
|
||||||
pub const WINDOW_MAX_HEIGHT: f32 = 500.0;
|
|
||||||
|
|
||||||
pub const CORNER_RADIUS: u8 = 8;
|
|
||||||
|
|
||||||
pub const FONT_SIZE_INPUT_WIDTH: f32 = 24.0;
|
|
||||||
pub const DEFAULT_FONT_SIZE_STR: &str = "14";
|
|
||||||
|
|
||||||
pub const PREVIEW_AREA_MAX_HEIGHT: f32 = 150.0;
|
|
||||||
|
|
||||||
pub const INNER_MARGIN: i8 = 8;
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
use crate::app::TextEditor;
|
use crate::app::TextEditor;
|
||||||
use crate::ui::constants::*;
|
|
||||||
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) {
|
||||||
@ -7,58 +6,29 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
|
|
||||||
let mut should_close = false;
|
let mut should_close = false;
|
||||||
let mut query_changed = false;
|
let mut query_changed = false;
|
||||||
let mut should_focus_editor = false;
|
|
||||||
|
|
||||||
let just_opened = app.show_find && !app.prev_show_find;
|
egui::Window::new("Find")
|
||||||
|
|
||||||
if just_opened && !app.find_query.is_empty() {
|
|
||||||
app.update_find_matches();
|
|
||||||
if app.current_match_index.is_some() {
|
|
||||||
app.select_current_match(ctx);
|
|
||||||
app.should_select_current_match = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let focus_requested = ctx.memory(|mem| {
|
|
||||||
mem.data
|
|
||||||
.get_temp::<bool>(egui::Id::new("focus_find_input"))
|
|
||||||
.unwrap_or(false)
|
|
||||||
});
|
|
||||||
|
|
||||||
let top_right_pos = egui::Pos2::new(ctx.available_rect().right(), 22.0);
|
|
||||||
|
|
||||||
egui::Window::new("")
|
|
||||||
.collapsible(false)
|
.collapsible(false)
|
||||||
.resizable(false)
|
.resizable(false)
|
||||||
.movable(true)
|
.movable(true)
|
||||||
.title_bar(false)
|
.anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
|
||||||
.default_pos(top_right_pos)
|
|
||||||
.fade_in(true)
|
|
||||||
.fade_out(true)
|
|
||||||
.frame(egui::Frame {
|
.frame(egui::Frame {
|
||||||
fill: visuals.window_fill,
|
fill: visuals.window_fill,
|
||||||
stroke: visuals.window_stroke,
|
stroke: visuals.window_stroke,
|
||||||
corner_radius: egui::CornerRadius::same(CORNER_RADIUS),
|
corner_radius: egui::CornerRadius::same(8),
|
||||||
shadow: visuals.window_shadow,
|
shadow: visuals.window_shadow,
|
||||||
inner_margin: egui::Margin::same(INNER_MARGIN),
|
inner_margin: egui::Margin::same(16),
|
||||||
outer_margin: egui::Margin::same(0),
|
outer_margin: egui::Margin::same(0),
|
||||||
})
|
})
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.set_min_width(300.0);
|
||||||
let arrow_text = if app.show_replace_section {
|
|
||||||
"⏷"
|
|
||||||
} else {
|
|
||||||
"⏵"
|
|
||||||
};
|
|
||||||
if ui.button(arrow_text).clicked() {
|
|
||||||
app.show_replace_section = !app.show_replace_section;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
ui.label("Find:");
|
ui.label("Find:");
|
||||||
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(200.0)
|
||||||
.hint_text("Enter search text..."),
|
.hint_text("Enter search text..."),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -66,30 +36,17 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
query_changed = true;
|
query_changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if just_opened || focus_requested || app.focus_find {
|
if !response.has_focus() {
|
||||||
response.request_focus();
|
response.request_focus();
|
||||||
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();
|
||||||
response.request_focus();
|
response.request_focus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if app.show_replace_section {
|
ui.add_space(8.0);
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.add_space(SMALL);
|
|
||||||
ui.label("Replace:");
|
|
||||||
let _replace_response = ui.add(
|
|
||||||
egui::TextEdit::singleline(&mut app.replace_query)
|
|
||||||
.desired_width(250.0)
|
|
||||||
.hint_text("Enter replacement text..."),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.add_space(MEDIUM);
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
let case_sensitive_changed = ui
|
let case_sensitive_changed = ui
|
||||||
@ -98,27 +55,9 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
if case_sensitive_changed {
|
if case_sensitive_changed {
|
||||||
query_changed = true;
|
query_changed = true;
|
||||||
}
|
}
|
||||||
if app.show_replace_section {
|
|
||||||
ui.add_space(MEDIUM);
|
|
||||||
|
|
||||||
let replace_current_enabled =
|
|
||||||
!app.find_matches.is_empty() && app.current_match_index.is_some();
|
|
||||||
ui.add_enabled_ui(replace_current_enabled, |ui| {
|
|
||||||
if ui.button("Replace").clicked() {
|
|
||||||
app.replace_current_match(ctx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let replace_all_enabled = !app.find_matches.is_empty();
|
|
||||||
ui.add_enabled_ui(replace_all_enabled, |ui| {
|
|
||||||
if ui.button("Replace All").clicked() {
|
|
||||||
app.replace_all(ctx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(MEDIUM);
|
ui.add_space(8.0);
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
let match_text = if app.find_matches.is_empty() {
|
let match_text = if app.find_matches.is_empty() {
|
||||||
@ -136,23 +75,23 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
ui.label(egui::RichText::new(match_text).weak());
|
ui.label(egui::RichText::new(match_text).weak());
|
||||||
|
|
||||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
if ui.button("❌").clicked() {
|
if ui.button("✕").clicked() {
|
||||||
should_close = true;
|
should_close = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.add_space(SMALL);
|
ui.add_space(4.0);
|
||||||
|
|
||||||
let next_enabled = !app.find_matches.is_empty();
|
let next_enabled = !app.find_matches.is_empty();
|
||||||
ui.add_enabled_ui(next_enabled, |ui| {
|
ui.add_enabled_ui(next_enabled, |ui| {
|
||||||
if ui.button("Next").clicked() {
|
if ui.button("Next").clicked() {
|
||||||
app.find_next(ctx);
|
app.find_next();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let prev_enabled = !app.find_matches.is_empty();
|
let prev_enabled = !app.find_matches.is_empty();
|
||||||
ui.add_enabled_ui(prev_enabled, |ui| {
|
ui.add_enabled_ui(prev_enabled, |ui| {
|
||||||
if ui.button("Previous").clicked() {
|
if ui.button("Previous").clicked() {
|
||||||
app.find_previous(ctx);
|
app.find_previous();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -162,28 +101,21 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
|
|
||||||
if query_changed {
|
if query_changed {
|
||||||
app.update_find_matches();
|
app.update_find_matches();
|
||||||
if app.current_match_index.is_some() {
|
|
||||||
app.select_current_match(ctx);
|
|
||||||
app.should_select_current_match = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if should_close {
|
if should_close {
|
||||||
app.select_current_match(ctx);
|
|
||||||
app.should_select_current_match = true;
|
|
||||||
app.show_find = false;
|
app.show_find = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.input(|i| {
|
ctx.input(|i| {
|
||||||
if i.key_pressed(egui::Key::Enter) && i.modifiers.ctrl && app.show_find {
|
if i.key_pressed(egui::Key::Escape) {
|
||||||
should_focus_editor = true;
|
app.show_find = false;
|
||||||
app.should_select_current_match = true;
|
} else if i.key_pressed(egui::Key::F3) {
|
||||||
|
if i.modifiers.shift {
|
||||||
|
app.find_previous();
|
||||||
|
} else {
|
||||||
|
app.find_next();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if should_focus_editor {
|
|
||||||
ctx.memory_mut(|mem| {
|
|
||||||
mem.request_focus(egui::Id::new("main_text_editor"));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
use crate::{app::TextEditor, io};
|
use crate::{app::TextEditor, io};
|
||||||
use eframe::egui::{self, Frame};
|
use eframe::egui::{self, Frame};
|
||||||
use egui::UiKind;
|
|
||||||
|
|
||||||
pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||||
let now = std::time::Instant::now();
|
let now = std::time::Instant::now();
|
||||||
@ -44,34 +43,34 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
egui::MenuBar::new().ui(ui, |ui| {
|
egui::menu::bar(ui, |ui| {
|
||||||
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);
|
io::new_file(app);
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
if ui.button("Open...").clicked() {
|
if ui.button("Open...").clicked() {
|
||||||
io::open_file(app);
|
io::open_file(app);
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if ui.button("Save").clicked() {
|
if ui.button("Save").clicked() {
|
||||||
io::save_file(app);
|
io::save_file(app);
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
if ui.button("Save As...").clicked() {
|
if ui.button("Save As...").clicked() {
|
||||||
io::save_as_file(app);
|
io::save_as_file(app);
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if ui.button("Preferences").clicked() {
|
if ui.button("Preferences").clicked() {
|
||||||
app.show_preferences = true;
|
app.show_preferences = true;
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
if ui.button("Exit").clicked() {
|
if ui.button("Exit").clicked() {
|
||||||
app.request_quit(ctx);
|
app.request_quit(ctx);
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -79,16 +78,16 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
app.menu_interaction_active = true;
|
app.menu_interaction_active = true;
|
||||||
if ui.button("Cut").clicked() {
|
if ui.button("Cut").clicked() {
|
||||||
ui.ctx().input_mut(|i| i.events.push(egui::Event::Cut));
|
ui.ctx().input_mut(|i| i.events.push(egui::Event::Cut));
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
if ui.button("Copy").clicked() {
|
if ui.button("Copy").clicked() {
|
||||||
ui.ctx().input_mut(|i| i.events.push(egui::Event::Copy));
|
ui.ctx().input_mut(|i| i.events.push(egui::Event::Copy));
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
if ui.button("Paste").clicked() {
|
if ui.button("Paste").clicked() {
|
||||||
ui.ctx()
|
ui.ctx()
|
||||||
.send_viewport_cmd(egui::ViewportCommand::RequestPaste);
|
.send_viewport_cmd(egui::ViewportCommand::RequestPaste);
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
if ui.button("Delete").clicked() {
|
if ui.button("Delete").clicked() {
|
||||||
ui.ctx().input_mut(|i| {
|
ui.ctx().input_mut(|i| {
|
||||||
@ -100,7 +99,7 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
modifiers: egui::Modifiers::NONE,
|
modifiers: egui::Modifiers::NONE,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
if ui.button("Select All").clicked() {
|
if ui.button("Select All").clicked() {
|
||||||
let text_edit_id = egui::Id::new("main_text_editor");
|
let text_edit_id = egui::Id::new("main_text_editor");
|
||||||
@ -117,7 +116,7 @@ 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_menu();
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if ui.button("Undo").clicked() {
|
if ui.button("Undo").clicked() {
|
||||||
@ -128,24 +127,21 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
if let Some(active_tab) = app.get_active_tab_mut() {
|
if let Some(active_tab) = app.get_active_tab_mut() {
|
||||||
let current_state = (
|
let current_state = (
|
||||||
state.cursor.char_range().unwrap_or_default(),
|
state.cursor.char_range().unwrap_or_default(),
|
||||||
active_tab.content.to_string(),
|
active_tab.content.clone(),
|
||||||
);
|
);
|
||||||
let mut undoer = state.undoer();
|
let mut undoer = state.undoer();
|
||||||
if let Some((cursor_range, content)) =
|
if let Some((cursor_range, content)) =
|
||||||
undoer.undo(¤t_state)
|
undoer.undo(¤t_state)
|
||||||
{
|
{
|
||||||
active_tab.content = content.to_string();
|
active_tab.content = content.clone();
|
||||||
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() {
|
|
||||||
app.update_find_matches();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
if ui.button("Redo").clicked() {
|
if ui.button("Redo").clicked() {
|
||||||
let text_edit_id = egui::Id::new("main_text_editor");
|
let text_edit_id = egui::Id::new("main_text_editor");
|
||||||
@ -155,24 +151,21 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
if let Some(active_tab) = app.get_active_tab_mut() {
|
if let Some(active_tab) = app.get_active_tab_mut() {
|
||||||
let current_state = (
|
let current_state = (
|
||||||
state.cursor.char_range().unwrap_or_default(),
|
state.cursor.char_range().unwrap_or_default(),
|
||||||
active_tab.content.to_string(),
|
active_tab.content.clone(),
|
||||||
);
|
);
|
||||||
let mut undoer = state.undoer();
|
let mut undoer = state.undoer();
|
||||||
if let Some((cursor_range, content)) =
|
if let Some((cursor_range, content)) =
|
||||||
undoer.redo(¤t_state)
|
undoer.redo(¤t_state)
|
||||||
{
|
{
|
||||||
active_tab.content = content.to_string();
|
active_tab.content = content.clone();
|
||||||
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() {
|
|
||||||
app.update_find_matches();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -183,29 +176,25 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
app.save_config();
|
app.save_config();
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
|
||||||
if ui
|
|
||||||
.checkbox(&mut app.syntax_highlighting, "Syntax Highlighting")
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
app.save_config();
|
|
||||||
ui.close_kind(UiKind::Menu);
|
|
||||||
}
|
}
|
||||||
if ui.checkbox(&mut app.word_wrap, "Word Wrap").clicked() {
|
if ui.checkbox(&mut app.word_wrap, "Word Wrap").clicked() {
|
||||||
app.save_config();
|
app.save_config();
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
if ui.checkbox(&mut app.hide_tab_bar, "Hide Tab Bar").clicked() {
|
if ui
|
||||||
|
.checkbox(&mut app.auto_hide_tab_bar, "Hide Tab Bar")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
app.save_config();
|
app.save_config();
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
if ui
|
if ui
|
||||||
.checkbox(&mut app.auto_hide_toolbar, "Auto Hide Toolbar")
|
.checkbox(&mut app.auto_hide_toolbar, "Auto Hide Toolbar")
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
app.save_config();
|
app.save_config();
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
@ -213,7 +202,7 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
if ui.button("Reset Zoom").clicked() {
|
if ui.button("Reset Zoom").clicked() {
|
||||||
app.zoom_factor = 1.0;
|
app.zoom_factor = 1.0;
|
||||||
ctx.set_zoom_factor(1.0);
|
ctx.set_zoom_factor(1.0);
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
@ -233,7 +222,7 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
if current_theme != crate::app::theme::Theme::System {
|
if current_theme != crate::app::theme::Theme::System {
|
||||||
app.set_theme(ctx);
|
app.set_theme(ctx);
|
||||||
}
|
}
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
if ui
|
if ui
|
||||||
.radio_value(
|
.radio_value(
|
||||||
@ -246,7 +235,7 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
if current_theme != crate::app::theme::Theme::Light {
|
if current_theme != crate::app::theme::Theme::Light {
|
||||||
app.set_theme(ctx);
|
app.set_theme(ctx);
|
||||||
}
|
}
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
if ui
|
if ui
|
||||||
.radio_value(&mut app.theme, crate::app::theme::Theme::Dark, "Dark")
|
.radio_value(&mut app.theme, crate::app::theme::Theme::Dark, "Dark")
|
||||||
@ -255,16 +244,16 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
if current_theme != crate::app::theme::Theme::Dark {
|
if current_theme != crate::app::theme::Theme::Dark {
|
||||||
app.set_theme(ctx);
|
app.set_theme(ctx);
|
||||||
}
|
}
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if ui.radio_value(&mut app.line_side, false, "Left").clicked() {
|
if ui.radio_value(&mut app.line_side, false, "Left").clicked() {
|
||||||
app.save_config();
|
app.save_config();
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
if ui.radio_value(&mut app.line_side, true, "Right").clicked() {
|
if ui.radio_value(&mut app.line_side, true, "Right").clicked() {
|
||||||
app.save_config();
|
app.save_config();
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -273,24 +262,30 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
app.menu_interaction_active = true;
|
app.menu_interaction_active = true;
|
||||||
if ui.button("Shortcuts").clicked() {
|
if ui.button("Shortcuts").clicked() {
|
||||||
app.show_shortcuts = true;
|
app.show_shortcuts = true;
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
if ui.button("About").clicked() {
|
if ui.button("About").clicked() {
|
||||||
app.show_about = true;
|
app.show_about = true;
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if app.hide_tab_bar {
|
if app.auto_hide_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.title.clone()
|
||||||
} else {
|
} else {
|
||||||
let empty_tab = crate::app::tab::Tab::new_empty(1);
|
let empty_tab = crate::app::tab::Tab::new_empty(1);
|
||||||
empty_tab.get_display_title()
|
empty_tab.title.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
let window_width = ctx.screen_rect().width();
|
let window_width = ctx.screen_rect().width();
|
||||||
let font_id = ui.style().text_styles[&egui::TextStyle::Body].to_owned();
|
let font_id = ui.style().text_styles[&egui::TextStyle::Body].clone();
|
||||||
|
|
||||||
|
let tab_title = if app.get_active_tab().is_some_and(|tab| tab.is_modified) {
|
||||||
|
format!("{tab_title}*")
|
||||||
|
} else {
|
||||||
|
tab_title
|
||||||
|
};
|
||||||
|
|
||||||
let text_galley = ui.fonts(|fonts| {
|
let text_galley = ui.fonts(|fonts| {
|
||||||
fonts.layout_job(egui::text::LayoutJob::simple_singleline(
|
fonts.layout_job(egui::text::LayoutJob::simple_singleline(
|
||||||
|
|||||||
@ -1,14 +1,11 @@
|
|||||||
use crate::app::TextEditor;
|
use crate::app::TextEditor;
|
||||||
use crate::ui::constants::*;
|
|
||||||
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.screen_rect();
|
||||||
let window_width =
|
let window_width = (screen_rect.width() * 0.6).min(400.0).max(300.0);
|
||||||
(screen_rect.width() * WINDOW_WIDTH_RATIO).clamp(WINDOW_MIN_WIDTH, WINDOW_MAX_WIDTH);
|
let window_height = (screen_rect.height() * 0.7).min(500.0).max(250.0);
|
||||||
let window_height =
|
|
||||||
(screen_rect.height() * WINDOW_HEIGHT_RATIO).clamp(WINDOW_MIN_HEIGHT, WINDOW_MAX_HEIGHT);
|
|
||||||
let max_size = egui::Vec2::new(window_width, window_height);
|
let max_size = egui::Vec2::new(window_width, window_height);
|
||||||
|
|
||||||
egui::Window::new("Preferences")
|
egui::Window::new("Preferences")
|
||||||
@ -17,173 +14,110 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
.anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
|
.anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
|
||||||
.default_open(true)
|
.default_open(true)
|
||||||
.max_size(max_size)
|
.max_size(max_size)
|
||||||
.fade_in(true)
|
|
||||||
.fade_out(true)
|
|
||||||
.frame(egui::Frame {
|
.frame(egui::Frame {
|
||||||
fill: visuals.window_fill,
|
fill: visuals.window_fill,
|
||||||
stroke: visuals.window_stroke,
|
stroke: visuals.window_stroke,
|
||||||
corner_radius: egui::CornerRadius::same(CORNER_RADIUS),
|
corner_radius: egui::CornerRadius::same(8),
|
||||||
shadow: visuals.window_shadow,
|
shadow: visuals.window_shadow,
|
||||||
inner_margin: egui::Margin::same(INNER_MARGIN),
|
inner_margin: egui::Margin::same(16),
|
||||||
outer_margin: egui::Margin::same(0),
|
outer_margin: egui::Margin::same(0),
|
||||||
})
|
})
|
||||||
.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.vertical(|ui| {
|
|
||||||
if ui
|
|
||||||
.checkbox(&mut app.state_cache, "Maintain State")
|
|
||||||
.on_hover_text("Unsaved changes will be cached between sessions")
|
|
||||||
.changed()
|
|
||||||
{
|
|
||||||
app.save_config();
|
|
||||||
if !app.state_cache {
|
|
||||||
if let Err(e) = TextEditor::clear_state_cache() {
|
|
||||||
eprintln!("Failed to clear state cache: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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() {
|
|
||||||
app.save_config();
|
|
||||||
}
|
|
||||||
ui.add_space(SMALL);
|
|
||||||
if ui
|
|
||||||
.checkbox(&mut app.syntax_highlighting, "Syntax Highlighting")
|
|
||||||
.changed()
|
|
||||||
{
|
|
||||||
app.save_config();
|
|
||||||
}
|
|
||||||
ui.add_space(SMALL);
|
|
||||||
if ui
|
|
||||||
.checkbox(&mut app.hide_tab_bar, "Hide Tab Bar")
|
|
||||||
.on_hover_text(
|
|
||||||
"Hide the tab bar and show tab title in menu bar instead",
|
|
||||||
)
|
|
||||||
.changed()
|
|
||||||
{
|
|
||||||
app.save_config();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.add_space(SMALL);
|
|
||||||
ui.separator();
|
|
||||||
ui.add_space(LARGE);
|
|
||||||
ui.heading("Font Settings");
|
ui.heading("Font Settings");
|
||||||
ui.add_space(MEDIUM);
|
ui.add_space(8.0);
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.vertical(|ui| {
|
ui.label("Font Family:");
|
||||||
ui.label("Font Family:");
|
ui.add_space(5.0);
|
||||||
ui.add_space(SMALL);
|
|
||||||
ui.label("Font Size:");
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
let mut changed = false;
|
||||||
let mut changed = false;
|
egui::ComboBox::from_id_salt("font_family")
|
||||||
egui::ComboBox::from_id_salt("font_family")
|
.selected_text(&app.font_family)
|
||||||
.selected_text(&app.font_family)
|
.show_ui(ui, |ui| {
|
||||||
.show_ui(ui, |ui| {
|
if ui
|
||||||
if ui
|
.selectable_value(
|
||||||
.selectable_value(
|
&mut app.font_family,
|
||||||
&mut app.font_family,
|
"Proportional".to_string(),
|
||||||
"Proportional".to_string(),
|
"Proportional",
|
||||||
"Proportional",
|
)
|
||||||
)
|
.clicked()
|
||||||
.clicked()
|
{
|
||||||
{
|
changed = true;
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if ui
|
|
||||||
.selectable_value(
|
|
||||||
&mut app.font_family,
|
|
||||||
"Monospace".to_string(),
|
|
||||||
"Monospace",
|
|
||||||
)
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if app.font_size_input.is_none() {
|
|
||||||
app.font_size_input = Some(app.font_size.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut font_size_text = app
|
|
||||||
.font_size_input
|
|
||||||
.as_ref()
|
|
||||||
.unwrap_or(&DEFAULT_FONT_SIZE_STR.to_string())
|
|
||||||
.to_owned();
|
|
||||||
ui.add_space(SMALL);
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
let response = ui.add(
|
|
||||||
egui::TextEdit::singleline(&mut font_size_text)
|
|
||||||
.desired_width(FONT_SIZE_INPUT_WIDTH)
|
|
||||||
.hint_text(DEFAULT_FONT_SIZE_STR)
|
|
||||||
.id(egui::Id::new("font_size_input")),
|
|
||||||
);
|
|
||||||
|
|
||||||
app.font_size_input = Some(font_size_text.to_owned());
|
|
||||||
|
|
||||||
if response.clicked() {
|
|
||||||
response.request_focus();
|
|
||||||
}
|
}
|
||||||
|
if ui
|
||||||
ui.label("px");
|
.selectable_value(
|
||||||
|
&mut app.font_family,
|
||||||
if response.lost_focus() {
|
"Monospace".to_string(),
|
||||||
if let Ok(new_size) = font_size_text.parse::<f32>() {
|
"Monospace",
|
||||||
let clamped_size = new_size.clamp(MIN_FONT_SIZE, MAX_FONT_SIZE);
|
)
|
||||||
if (app.font_size - clamped_size).abs() > 0.1 {
|
.clicked()
|
||||||
app.font_size = clamped_size;
|
{
|
||||||
app.apply_font_settings(ctx);
|
changed = true;
|
||||||
}
|
|
||||||
}
|
|
||||||
app.font_size_input = None;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if changed {
|
if changed {
|
||||||
app.apply_font_settings(ctx);
|
app.apply_font_settings_with_ui(ctx, ui);
|
||||||
}
|
}
|
||||||
})
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(MEDIUM);
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Font Size:");
|
||||||
|
ui.add_space(5.0);
|
||||||
|
|
||||||
|
if app.font_size_input.is_none() {
|
||||||
|
app.font_size_input = Some(app.font_size.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut font_size_text = app
|
||||||
|
.font_size_input
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&"14".to_string())
|
||||||
|
.clone();
|
||||||
|
let response = ui.add(
|
||||||
|
egui::TextEdit::singleline(&mut font_size_text)
|
||||||
|
.desired_width(50.0)
|
||||||
|
.hint_text("14")
|
||||||
|
.id(egui::Id::new("font_size_input")),
|
||||||
|
);
|
||||||
|
|
||||||
|
app.font_size_input = Some(font_size_text.clone());
|
||||||
|
|
||||||
|
if response.clicked() {
|
||||||
|
response.request_focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.label("px");
|
||||||
|
|
||||||
|
if response.lost_focus() {
|
||||||
|
if let Ok(new_size) = font_size_text.parse::<f32>() {
|
||||||
|
let clamped_size = new_size.clamp(8.0, 32.0);
|
||||||
|
if (app.font_size - clamped_size).abs() > 0.1 {
|
||||||
|
app.font_size = clamped_size;
|
||||||
|
app.apply_font_settings_with_ui(ctx, ui);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.font_size_input = None;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(12.0);
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
ui.add_space(8.0);
|
||||||
ui.label("Preview:");
|
ui.label("Preview:");
|
||||||
ui.add_space(SMALL);
|
ui.add_space(4.0);
|
||||||
|
|
||||||
egui::ScrollArea::vertical()
|
egui::ScrollArea::vertical()
|
||||||
.max_height(PREVIEW_AREA_MAX_HEIGHT)
|
.max_height(150.0)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
egui::Frame::new()
|
egui::Frame::new()
|
||||||
.fill(visuals.code_bg_color)
|
.fill(visuals.code_bg_color)
|
||||||
.stroke(visuals.widgets.noninteractive.bg_stroke)
|
.stroke(visuals.widgets.noninteractive.bg_stroke)
|
||||||
.inner_margin(egui::Margin::same(INNER_MARGIN))
|
.inner_margin(egui::Margin::same(8))
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
let preview_font = egui::FontId::new(
|
let preview_font = egui::FontId::new(
|
||||||
app.font_size,
|
app.font_size,
|
||||||
@ -196,24 +130,23 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
egui::RichText::new(
|
egui::RichText::new(
|
||||||
"The quick brown fox jumps over the lazy dog.",
|
"The quick brown fox jumps over the lazy dog.",
|
||||||
)
|
)
|
||||||
.font(preview_font.to_owned()),
|
.font(preview_font.clone()),
|
||||||
);
|
);
|
||||||
ui.label(
|
ui.label(
|
||||||
egui::RichText::new("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
egui::RichText::new("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||||
.font(preview_font.to_owned()),
|
.font(preview_font.clone()),
|
||||||
);
|
);
|
||||||
ui.label(
|
ui.label(
|
||||||
egui::RichText::new("abcdefghijklmnopqrstuvwxyz")
|
egui::RichText::new("abcdefghijklmnopqrstuvwxyz")
|
||||||
.font(preview_font.to_owned()),
|
.font(preview_font.clone()),
|
||||||
);
|
);
|
||||||
ui.label(
|
ui.label(
|
||||||
egui::RichText::new("1234567890 !@#$%^&*()")
|
egui::RichText::new("1234567890 !@#$%^&*()").font(preview_font),
|
||||||
.font(preview_font.to_owned()),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(LARGE);
|
ui.add_space(12.0);
|
||||||
|
|
||||||
if ui.button("Close").clicked() {
|
if ui.button("Close").clicked() {
|
||||||
app.show_preferences = false;
|
app.show_preferences = false;
|
||||||
|
|||||||
@ -1,48 +1,39 @@
|
|||||||
use crate::app::TextEditor;
|
use crate::app::TextEditor;
|
||||||
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) {
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
ui.label(
|
ui.label(egui::RichText::new("Navigation").size(18.0).strong());
|
||||||
egui::RichText::new("Navigation")
|
ui.label(egui::RichText::new("Ctrl + N: New").size(14.0));
|
||||||
.size(UI_HEADER_SIZE)
|
ui.label(egui::RichText::new("Ctrl + O: Open").size(14.0));
|
||||||
.strong(),
|
ui.label(egui::RichText::new("Ctrl + S: Save").size(14.0));
|
||||||
);
|
ui.label(egui::RichText::new("Ctrl + Shift + S: Save As").size(14.0));
|
||||||
ui.label(egui::RichText::new("Ctrl + N: New").size(UI_TEXT_SIZE));
|
ui.label(egui::RichText::new("Ctrl + T: New Tab").size(14.0));
|
||||||
ui.label(egui::RichText::new("Ctrl + O: Open").size(UI_TEXT_SIZE));
|
ui.label(egui::RichText::new("Ctrl + Tab: Next Tab").size(14.0));
|
||||||
ui.label(egui::RichText::new("Ctrl + S: Save").size(UI_TEXT_SIZE));
|
ui.label(egui::RichText::new("Ctrl + Shift + Tab: Last Tab").size(14.0));
|
||||||
ui.label(egui::RichText::new("Ctrl + Shift + S: Save As").size(UI_TEXT_SIZE));
|
ui.add_space(16.0);
|
||||||
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.separator();
|
||||||
|
|
||||||
ui.label(egui::RichText::new("Editing").size(UI_HEADER_SIZE).strong());
|
ui.label(egui::RichText::new("Editing").size(18.0).strong());
|
||||||
ui.label(egui::RichText::new("Ctrl + Z: Undo").size(UI_TEXT_SIZE));
|
ui.label(egui::RichText::new("Ctrl + Z: Undo").size(14.0));
|
||||||
ui.label(egui::RichText::new("Ctrl + Shift + Z: Redo").size(UI_TEXT_SIZE));
|
ui.label(egui::RichText::new("Ctrl + Shift + Z: Redo").size(14.0));
|
||||||
ui.label(egui::RichText::new("Ctrl + X: Cut").size(UI_TEXT_SIZE));
|
ui.label(egui::RichText::new("Ctrl + X: Cut").size(14.0));
|
||||||
ui.label(egui::RichText::new("Ctrl + C: Copy").size(UI_TEXT_SIZE));
|
ui.label(egui::RichText::new("Ctrl + C: Copy").size(14.0));
|
||||||
ui.label(egui::RichText::new("Ctrl + V: Paste").size(UI_TEXT_SIZE));
|
ui.label(egui::RichText::new("Ctrl + V: Paste").size(14.0));
|
||||||
ui.label(egui::RichText::new("Ctrl + A: Select All").size(UI_TEXT_SIZE));
|
ui.label(egui::RichText::new("Ctrl + A: Select All").size(14.0));
|
||||||
ui.label(egui::RichText::new("Ctrl + D: Delete Line").size(UI_TEXT_SIZE));
|
ui.label(egui::RichText::new("Ctrl + D: Delete Line").size(14.0));
|
||||||
ui.label(egui::RichText::new("Ctrl + F: Find").size(UI_TEXT_SIZE));
|
|
||||||
ui.label(egui::RichText::new("Ctrl + R: Replace").size(UI_TEXT_SIZE));
|
|
||||||
|
|
||||||
ui.add_space(VLARGE);
|
ui.add_space(16.0);
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.label(egui::RichText::new("Views").size(UI_HEADER_SIZE).strong());
|
ui.label(egui::RichText::new("Views").size(18.0).strong());
|
||||||
ui.label(egui::RichText::new("Ctrl + L: Toggle Line Numbers").size(UI_TEXT_SIZE));
|
ui.label(egui::RichText::new("Ctrl + L: Toggle Line Numbers").size(14.0));
|
||||||
ui.label(
|
ui.label(egui::RichText::new("Ctrl + Shift + L: Change Line Number Side").size(14.0));
|
||||||
egui::RichText::new("Ctrl + Shift + L: Change Line Number Side").size(UI_TEXT_SIZE),
|
ui.label(egui::RichText::new("Ctrl + K: Toggle Word Wrap").size(14.0));
|
||||||
);
|
ui.label(egui::RichText::new("Ctrl + H: Toggle Auto Hide Toolbar").size(14.0));
|
||||||
ui.label(egui::RichText::new("Ctrl + K: Toggle Word Wrap").size(UI_TEXT_SIZE));
|
ui.label(egui::RichText::new("Ctrl + P: Preferences").size(14.0));
|
||||||
ui.label(egui::RichText::new("Ctrl + H: Toggle Auto Hide Toolbar").size(UI_TEXT_SIZE));
|
ui.label(egui::RichText::new("Ctrl + =/-: Increase/Decrease Font Size").size(14.0));
|
||||||
ui.label(egui::RichText::new("Ctrl + P: Preferences").size(UI_TEXT_SIZE));
|
ui.label(egui::RichText::new("Ctrl + Shift + =/-: Zoom In/Out").size(14.0));
|
||||||
ui.label(egui::RichText::new("Ctrl + =/-: Increase/Decrease Font Size").size(UI_TEXT_SIZE));
|
|
||||||
ui.label(egui::RichText::new("Ctrl + Shift + =/-: Zoom In/Out").size(UI_TEXT_SIZE));
|
|
||||||
// ui.label(
|
// ui.label(
|
||||||
// egui::RichText::new("Ctrl + Shift + .: Toggle Vim Mode")
|
// egui::RichText::new("Ctrl + Shift + .: Toggle Vim Mode")
|
||||||
// .size(14.0)
|
// .size(14.0)
|
||||||
@ -51,8 +42,7 @@ fn render_shortcuts_content(ui: &mut egui::Ui) {
|
|||||||
// egui::RichText::new("Ctrl + .: Toggle Vim Mode")
|
// egui::RichText::new("Ctrl + .: Toggle Vim Mode")
|
||||||
// .size(14.0)
|
// .size(14.0)
|
||||||
// );
|
// );
|
||||||
ui.add_space(VLARGE);
|
ui.add_space(12.0);
|
||||||
ui.separator();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,29 +50,27 @@ 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.screen_rect();
|
||||||
|
|
||||||
let window_width =
|
// Calculate appropriate window size that always fits nicely in the main window
|
||||||
(screen_rect.width() * WINDOW_WIDTH_RATIO).clamp(WINDOW_MIN_WIDTH, WINDOW_MAX_WIDTH);
|
let window_width = (screen_rect.width() * 0.6).clamp(300.0, 400.0);
|
||||||
let window_height =
|
let window_height = (screen_rect.height() * 0.7).clamp(250.0, 500.0);
|
||||||
(screen_rect.height() * WINDOW_HEIGHT_RATIO).clamp(WINDOW_MIN_HEIGHT, WINDOW_MAX_HEIGHT);
|
|
||||||
|
|
||||||
egui::Window::new("Shortcuts")
|
egui::Window::new("Shortcuts")
|
||||||
.collapsible(false)
|
.collapsible(false)
|
||||||
.resizable(false)
|
.resizable(false)
|
||||||
.anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
|
.anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
|
||||||
.fixed_size([window_width, window_height])
|
.fixed_size([window_width, window_height])
|
||||||
.fade_in(true)
|
|
||||||
.fade_out(true)
|
|
||||||
.frame(egui::Frame {
|
.frame(egui::Frame {
|
||||||
fill: visuals.window_fill,
|
fill: visuals.window_fill,
|
||||||
stroke: visuals.window_stroke,
|
stroke: visuals.window_stroke,
|
||||||
corner_radius: egui::CornerRadius::same(CORNER_RADIUS),
|
corner_radius: egui::CornerRadius::same(8),
|
||||||
shadow: visuals.window_shadow,
|
shadow: visuals.window_shadow,
|
||||||
inner_margin: egui::Margin::same(INNER_MARGIN),
|
inner_margin: egui::Margin::same(16),
|
||||||
outer_margin: egui::Margin::same(0),
|
outer_margin: egui::Margin::same(0),
|
||||||
})
|
})
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
let available_height = ui.available_height() - 40.0;
|
// Scrollable content area
|
||||||
|
let available_height = ui.available_height() - 40.0; // Reserve space for close button
|
||||||
ui.allocate_ui_with_layout(
|
ui.allocate_ui_with_layout(
|
||||||
[ui.available_width(), available_height].into(),
|
[ui.available_width(), available_height].into(),
|
||||||
egui::Layout::top_down(egui::Align::Center),
|
egui::Layout::top_down(egui::Align::Center),
|
||||||
@ -95,8 +83,9 @@ pub(crate) fn shortcuts_window(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Fixed close button at bottom
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
ui.add_space(MEDIUM);
|
ui.add_space(8.0);
|
||||||
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)
|
||||||
|
|||||||
@ -3,98 +3,86 @@ use eframe::egui::{self, Frame};
|
|||||||
|
|
||||||
pub(crate) fn tab_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
pub(crate) fn tab_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||||
let frame = Frame::NONE.fill(ctx.style().visuals.panel_fill);
|
let frame = Frame::NONE.fill(ctx.style().visuals.panel_fill);
|
||||||
let tab_bar = egui::TopBottomPanel::top("tab_bar")
|
let response = egui::TopBottomPanel::top("tab_bar")
|
||||||
.frame(frame)
|
.frame(frame)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
egui::ScrollArea::horizontal()
|
ui.horizontal(|ui| {
|
||||||
.auto_shrink([false, true])
|
let mut tab_to_close_unmodified = None;
|
||||||
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden)
|
let mut tab_to_close_modified = None;
|
||||||
.scroll_source(egui::scroll_area::ScrollSource::DRAG)
|
let mut tab_to_switch = None;
|
||||||
.show(ui, |ui| {
|
let mut add_new_tab = false;
|
||||||
ui.horizontal(|ui| {
|
|
||||||
let mut tab_to_close_unmodified = None;
|
|
||||||
let mut tab_to_close_modified = None;
|
|
||||||
let mut tab_to_switch = None;
|
|
||||||
let mut add_new_tab = false;
|
|
||||||
|
|
||||||
let tabs_len = app.tabs.len();
|
let tabs_len = app.tabs.len();
|
||||||
let active_tab_index = app.active_tab_index;
|
let active_tab_index = app.active_tab_index;
|
||||||
|
|
||||||
let tabs_info: Vec<(String, bool)> = app
|
let tabs_info: Vec<(String, bool)> = app
|
||||||
.tabs
|
.tabs
|
||||||
.iter()
|
.iter()
|
||||||
.map(|tab| (tab.get_display_title(), tab.is_modified))
|
.map(|tab| (tab.get_display_title(), tab.is_modified))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
for (i, (title, is_modified)) in tabs_info.iter().enumerate() {
|
for (i, (title, is_modified)) in tabs_info.iter().enumerate() {
|
||||||
let is_active = i == active_tab_index;
|
let is_active = i == active_tab_index;
|
||||||
|
|
||||||
let mut label_text = if is_active {
|
let mut label_text = if is_active {
|
||||||
egui::RichText::new(title).strong()
|
egui::RichText::new(title).strong()
|
||||||
} else {
|
} else {
|
||||||
egui::RichText::new(title).color(ui.visuals().weak_text_color())
|
egui::RichText::new(title).color(ui.visuals().weak_text_color())
|
||||||
};
|
};
|
||||||
|
|
||||||
if *is_modified {
|
if *is_modified {
|
||||||
label_text = label_text.italics();
|
label_text = label_text.italics();
|
||||||
}
|
}
|
||||||
|
|
||||||
let tab_response = ui.add(
|
let tab_response =
|
||||||
egui::Label::new(label_text)
|
ui.add(egui::Label::new(label_text).sense(egui::Sense::click()));
|
||||||
.selectable(false)
|
if tab_response.clicked() {
|
||||||
.sense(egui::Sense::click()),
|
tab_to_switch = Some(i);
|
||||||
);
|
}
|
||||||
if tab_response.clicked() {
|
|
||||||
tab_to_switch = Some(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
if tabs_len > 1 {
|
|
||||||
let visuals = ui.visuals();
|
|
||||||
let close_button = egui::Button::new("×")
|
|
||||||
.small()
|
|
||||||
.fill(visuals.panel_fill)
|
|
||||||
.stroke(egui::Stroke::new(
|
|
||||||
0.0,
|
|
||||||
egui::Color32::from_rgb(0, 0, 0),
|
|
||||||
));
|
|
||||||
let close_response = ui.add(close_button);
|
|
||||||
if close_response.clicked() {
|
|
||||||
if *is_modified {
|
|
||||||
tab_to_close_modified = Some(i);
|
|
||||||
} else {
|
|
||||||
tab_to_close_unmodified = Some(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if tabs_len > 1 {
|
||||||
let visuals = ui.visuals();
|
let visuals = ui.visuals();
|
||||||
let add_button = egui::Button::new("+")
|
let close_button = egui::Button::new("×")
|
||||||
.small()
|
.small()
|
||||||
.fill(visuals.panel_fill)
|
.fill(visuals.panel_fill)
|
||||||
.stroke(egui::Stroke::new(0.0, egui::Color32::from_rgb(0, 0, 0)));
|
.stroke(egui::Stroke::new(0.0, egui::Color32::from_rgb(0, 0, 0)));
|
||||||
if ui.add(add_button).clicked() {
|
let close_response = ui.add(close_button);
|
||||||
add_new_tab = true;
|
if close_response.clicked() {
|
||||||
|
if *is_modified {
|
||||||
|
tab_to_close_modified = Some(i);
|
||||||
|
} else {
|
||||||
|
tab_to_close_unmodified = Some(i);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(tab_index) = tab_to_switch {
|
ui.separator();
|
||||||
app.switch_to_tab(tab_index);
|
}
|
||||||
}
|
|
||||||
if let Some(tab_index) = tab_to_close_unmodified {
|
let visuals = ui.visuals();
|
||||||
app.close_tab(tab_index);
|
let add_button = egui::Button::new("+")
|
||||||
}
|
.small()
|
||||||
if let Some(tab_index) = tab_to_close_modified {
|
.fill(visuals.panel_fill)
|
||||||
app.switch_to_tab(tab_index);
|
.stroke(egui::Stroke::new(0.0, egui::Color32::from_rgb(0, 0, 0)));
|
||||||
app.pending_unsaved_action = Some(UnsavedAction::CloseTab(tab_index));
|
if ui.add(add_button).clicked() {
|
||||||
}
|
add_new_tab = true;
|
||||||
if add_new_tab {
|
}
|
||||||
app.add_new_tab();
|
|
||||||
}
|
if let Some(tab_index) = tab_to_switch {
|
||||||
});
|
app.switch_to_tab(tab_index);
|
||||||
});
|
}
|
||||||
|
if let Some(tab_index) = tab_to_close_unmodified {
|
||||||
|
app.close_tab(tab_index);
|
||||||
|
}
|
||||||
|
if let Some(tab_index) = tab_to_close_modified {
|
||||||
|
app.switch_to_tab(tab_index);
|
||||||
|
app.pending_unsaved_action = Some(UnsavedAction::CloseTab(tab_index));
|
||||||
|
}
|
||||||
|
if add_new_tab {
|
||||||
|
app.add_new_tab();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.tab_bar_rect = Some(tab_bar.response.rect);
|
app.tab_bar_rect = Some(response.response.rect);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user