Compare commits
No commits in common. "release" and "0.0.3" have entirely different histories.
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,3 @@
|
|||||||
.c*
|
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
/target
|
/target
|
||||||
perf.*
|
perf.*
|
||||||
|
|||||||
22
Cargo.toml
22
Cargo.toml
@ -1,19 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ced"
|
name = "ced"
|
||||||
version = "0.1.3"
|
version = "0.0.3"
|
||||||
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"] }
|
|
||||||
|
|||||||
25
LICENSE-MIT
25
LICENSE-MIT
@ -1,25 +0,0 @@
|
|||||||
Copyright (c) Filip Bicki <candle@lampnet.io>
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any
|
|
||||||
person obtaining a copy of this software and associated
|
|
||||||
documentation files (the "Software"), to deal in the
|
|
||||||
Software without restriction, including without
|
|
||||||
limitation the rights to use, copy, modify, merge,
|
|
||||||
publish, distribute, sublicense, and/or sell copies of
|
|
||||||
the Software, and to permit persons to whom the Software
|
|
||||||
is furnished to do so, subject to the following
|
|
||||||
conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice
|
|
||||||
shall be included in all copies or substantial portions
|
|
||||||
of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
|
||||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
|
||||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
|
||||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
||||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
||||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
|
||||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
||||||
DEALINGS IN THE SOFTWARE.
|
|
||||||
78
README.md
78
README.md
@ -1,78 +0,0 @@
|
|||||||
# ced - candle's Editor
|
|
||||||
|
|
||||||
There is a disturbing lack of simple GUI text editors available on Linux natively. The world of TUI editors is flourishing, but regular people don't 'yank to system register' when they want to move text from one file to another. In the world of GUI text editors you have a few options, all with their own caveats:\
|
|
||||||
`gedit` -> Good for the GNOME Desktop, but uses GTK-4.0 so it stands out anywhere else.\
|
|
||||||
`Kate` -> If you're not on KDE already, it comes with tons of overhead (52 packages on Arch Linux for an application to write text).\
|
|
||||||
`Emacs` -> Requires a degree in understanding its documentation.
|
|
||||||
|
|
||||||
(c)andle's (Ed)itor aims to be a single ~~hopefully small~~ binary for that one purpose.
|
|
||||||
## Features
|
|
||||||
|
|
||||||
* 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.
|
|
||||||
* Separate UI zoom that doesn't affect font size (`Ctrl+Shift` + `+`/`-`).
|
|
||||||
* Ricers rejoice, your `pywal` colors will be used!
|
|
||||||
* Weirdly smooth typing experience.
|
|
||||||
|
|
||||||
## Build and Install
|
|
||||||
##### Requirements
|
|
||||||
`git`, `rust`/`rustup`/`cargo`
|
|
||||||
##### Arch Linux
|
|
||||||
`sudo pacman -S git rust`
|
|
||||||
##### Ubuntu/Debian
|
|
||||||
`sudo apt install git rust`
|
|
||||||
|
|
||||||
#### Install
|
|
||||||
```bash
|
|
||||||
git clone https://code.lampnet.io/candle/ced
|
|
||||||
cd ced && cargo build --release
|
|
||||||
sudo mv target/release/ced /usr/local/bin/
|
|
||||||
sudo install -Dm644 ced.desktop /usr/share/applications/ced.desktop
|
|
||||||
```
|
|
||||||
|
|
||||||
`ced` should now appear as 'Text Editor' in your application launcher. You can remove the cloned directory at this point.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
`ced` will look for, and create if needed, a configuration file at: `$XDG_CONFIG_HOME/ced/config.toml`.
|
|
||||||
|
|
||||||
Here is an example `config.toml`:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
state_cache = true
|
|
||||||
auto_hide_toolbar = false
|
|
||||||
show_line_numbers = false
|
|
||||||
word_wrap = false
|
|
||||||
theme = "System"
|
|
||||||
line_side = false
|
|
||||||
font_family = "Monospace"
|
|
||||||
font_size = 16.0
|
|
||||||
syntax_highlighting = true
|
|
||||||
```
|
|
||||||
|
|
||||||
### Options
|
|
||||||
|
|
||||||
| 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. |
|
|
||||||
| `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`. |
|
|
||||||
| `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. |
|
|
||||||
| `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_size` | `14.0` | `8.0-32.0` The font size for text editing. |
|
|
||||||
| `theme` | `"System"` | The color scheme for the application. Options: `"System"` (attempts to use colors from `$XDG_CACHE_HOME/wal/colors` if present, otherwise uses system's light/dark mode preference), `"Light"`, or `"Dark"` (manually specify a theme). |
|
|
||||||
|
|
||||||
## Future Plans
|
|
||||||
In order of importance.
|
|
||||||
| Feature | Info |
|
|
||||||
| ------- | ---- |
|
|
||||||
| **LSP:** | Looking at allowing you to use/attach your own tools for this. |
|
|
||||||
| **Choose Font** | More than just Monospace/Proportional. |
|
|
||||||
| **Vim Mode:** | It's in-escapable. |
|
|
||||||
| **CLI Mode:** | 💀 |
|
|
||||||
| **IDE MODE:** | 🤡 |
|
|
||||||
|
|
||||||
I use [Helix](https://helix-editor.com/), btw.
|
|
||||||
@ -5,71 +5,26 @@ 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 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(),
|
show_line_numbers: false,
|
||||||
hide_tab_bar: default_hide_tab_bar(),
|
word_wrap: true,
|
||||||
show_line_numbers: default_show_line_numbers(),
|
|
||||||
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 +33,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;
|
||||||
};
|
};
|
||||||
@ -97,14 +52,13 @@ impl Config {
|
|||||||
if !config_path.exists() {
|
if !config_path.exists() {
|
||||||
let default_config = Self::default();
|
let default_config = Self::default();
|
||||||
if let Err(e) = default_config.save() {
|
if let Err(e) = default_config.save() {
|
||||||
eprintln!("Failed to create default config file: {e}");
|
eprintln!("Failed to create default config file: {}", e);
|
||||||
}
|
}
|
||||||
return default_config;
|
return default_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!(
|
||||||
@ -112,14 +66,9 @@ impl Config {
|
|||||||
config_path.display(),
|
config_path.display(),
|
||||||
e
|
e
|
||||||
);
|
);
|
||||||
return Self::default();
|
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 +80,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,13 @@ 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 {
|
||||||
editor.pending_unsaved_action = Some(
|
// Show dialog for unsaved changes
|
||||||
super::state::UnsavedAction::CloseTab(editor.active_tab_index),
|
editor.pending_unsaved_action = Some(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 +249,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 +274,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 +298,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;
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
use super::editor::TextEditor;
|
|
||||||
use crate::app::shortcuts;
|
use crate::app::shortcuts;
|
||||||
use crate::ui::about_window::about_window;
|
use crate::ui::{
|
||||||
use crate::ui::central_panel::central_panel;
|
about_window::about_window, central_panel::central_panel, find_window::find_window,
|
||||||
use crate::ui::find_window::find_window;
|
menu_bar::menu_bar, preferences_window::preferences_window, shortcuts_window::shortcuts_window,
|
||||||
use crate::ui::menu_bar::menu_bar;
|
tab_bar::tab_bar,
|
||||||
use crate::ui::preferences_window::preferences_window;
|
};
|
||||||
use crate::ui::shortcuts_window::shortcuts_window;
|
use eframe::egui;
|
||||||
use crate::ui::tab_bar::tab_bar;
|
|
||||||
|
use super::editor::TextEditor;
|
||||||
|
|
||||||
impl eframe::App for TextEditor {
|
impl eframe::App for TextEditor {
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
@ -24,8 +24,77 @@ impl eframe::App for TextEditor {
|
|||||||
|
|
||||||
menu_bar(self, ctx);
|
menu_bar(self, ctx);
|
||||||
|
|
||||||
if !self.hide_tab_bar {
|
// if self.tabs.len() > 1 {
|
||||||
tab_bar(self, ctx);
|
tab_bar(self, ctx);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Extract data needed for calculations to avoid borrow conflicts
|
||||||
|
let (content_changed, layout_changed, needs_processing) = if let Some(active_tab) = self.get_active_tab() {
|
||||||
|
let content_changed = active_tab.last_content_hash != crate::app::tab::compute_content_hash(&active_tab.content, &mut std::hash::DefaultHasher::new());
|
||||||
|
let layout_changed = self.needs_width_calculation(ctx);
|
||||||
|
(content_changed, layout_changed, true)
|
||||||
|
} else {
|
||||||
|
(false, false, false)
|
||||||
|
};
|
||||||
|
|
||||||
|
if needs_processing {
|
||||||
|
// Only recalculate width when layout parameters change, not on every keystroke
|
||||||
|
if layout_changed {
|
||||||
|
let width = if self.word_wrap {
|
||||||
|
// For word wrap, width only depends on layout parameters
|
||||||
|
let total_width = ctx.available_rect().width();
|
||||||
|
if self.show_line_numbers {
|
||||||
|
let line_count = if let Some(tab) = self.get_active_tab() {
|
||||||
|
tab.content.lines().count().max(1)
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
};
|
||||||
|
let line_count_digits = line_count.to_string().len();
|
||||||
|
let estimated_char_width = self.font_size * 0.6;
|
||||||
|
let base_line_number_width = line_count_digits as f32 * estimated_char_width;
|
||||||
|
let line_number_width = if self.line_side {
|
||||||
|
base_line_number_width + 20.0
|
||||||
|
} else {
|
||||||
|
base_line_number_width + 8.0
|
||||||
|
};
|
||||||
|
(total_width - line_number_width - 10.0).max(100.0)
|
||||||
|
} else {
|
||||||
|
total_width
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For non-word wrap, use a generous fixed width to avoid constant recalculation
|
||||||
|
// This prevents cursor jumping while still allowing horizontal scrolling
|
||||||
|
let base_width = ctx.available_rect().width();
|
||||||
|
if self.show_line_numbers {
|
||||||
|
let estimated_char_width = self.font_size * 0.6;
|
||||||
|
let line_number_width = if self.line_side { 60.0 } else { 40.0 };
|
||||||
|
(base_width - line_number_width - 10.0).max(100.0)
|
||||||
|
} else {
|
||||||
|
base_width
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.update_width_calculation_state(ctx, width);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process text changes using stable cached width
|
||||||
|
if content_changed {
|
||||||
|
if let Some(active_tab) = self.get_active_tab() {
|
||||||
|
let content = active_tab.content.clone();
|
||||||
|
let word_wrap = self.word_wrap;
|
||||||
|
let cached_width = self.get_cached_width();
|
||||||
|
let available_width = cached_width.unwrap_or_else(|| {
|
||||||
|
// Initialize with a reasonable default if no cache exists
|
||||||
|
if word_wrap {
|
||||||
|
ctx.available_rect().width()
|
||||||
|
} else {
|
||||||
|
ctx.available_rect().width()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.process_text_for_rendering(&content, available_width);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
central_panel(self, ctx);
|
central_panel(self, ctx);
|
||||||
@ -46,6 +115,8 @@ 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,61 @@
|
|||||||
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,
|
|
||||||
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,
|
||||||
|
// Width calculation cache and state tracking
|
||||||
|
cached_width: None,
|
||||||
|
last_word_wrap: config.word_wrap,
|
||||||
|
last_show_line_numbers: config.show_line_numbers,
|
||||||
|
last_font_size: config.font_size,
|
||||||
|
last_line_side: config.line_side,
|
||||||
|
last_viewport_width: 0.0,
|
||||||
|
// vim_mode: config.vim_mode,
|
||||||
|
|
||||||
|
// Cursor tracking for smart scrolling
|
||||||
|
previous_cursor_position: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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));
|
||||||
@ -80,21 +72,20 @@ impl TextEditor {
|
|||||||
|
|
||||||
editor.apply_font_settings(&cc.egui_ctx);
|
editor.apply_font_settings(&cc.egui_ctx);
|
||||||
|
|
||||||
|
editor.start_text_processing_thread();
|
||||||
|
|
||||||
editor
|
editor
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
|
||||||
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -102,7 +93,7 @@ impl TextEditor {
|
|||||||
pub fn save_config(&self) {
|
pub fn save_config(&self) {
|
||||||
let config = self.get_config();
|
let config = self.get_config();
|
||||||
if let Err(e) = config.save() {
|
if let Err(e) = config.save() {
|
||||||
eprintln!("Failed to save configuration: {e}");
|
eprintln!("Failed to save configuration: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,6 @@ 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,
|
|
||||||
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(),
|
||||||
@ -32,23 +29,24 @@ impl Default for TextEditor {
|
|||||||
tab_bar_rect: None,
|
tab_bar_rect: None,
|
||||||
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,
|
// Width calculation cache and state tracking
|
||||||
|
cached_width: None,
|
||||||
|
last_word_wrap: true,
|
||||||
|
last_show_line_numbers: false,
|
||||||
|
last_font_size: 14.0,
|
||||||
|
last_line_side: false,
|
||||||
|
last_viewport_width: 0.0,
|
||||||
|
// vim_mode: false,
|
||||||
|
|
||||||
|
// Cursor tracking for smart scrolling
|
||||||
previous_cursor_position: None,
|
previous_cursor_position: None,
|
||||||
previous_content: String::new(),
|
|
||||||
previous_cursor_char_index: None,
|
|
||||||
current_cursor_line: 0,
|
|
||||||
previous_cursor_line: 0,
|
|
||||||
font_settings_changed: false,
|
|
||||||
text_needs_processing: false,
|
|
||||||
should_select_current_match: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,18 +13,18 @@ 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 visual_line_mapping: Vec<Option<usize>>,
|
||||||
pub longest_line_length: usize,
|
pub max_line_length: f32,
|
||||||
pub longest_line_pixel_width: f32,
|
pub _processed_content: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TextProcessingResult {
|
impl Default for TextProcessingResult {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
line_count: 1,
|
line_count: 1,
|
||||||
longest_line_index: 0,
|
visual_line_mapping: vec![Some(1)],
|
||||||
longest_line_length: 0,
|
max_line_length: 0.0,
|
||||||
longest_line_pixel_width: 0.0,
|
_processed_content: String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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,6 @@ 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) 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,
|
||||||
@ -57,21 +54,22 @@ pub struct TextEditor {
|
|||||||
pub(crate) tab_bar_rect: Option<egui::Rect>,
|
pub(crate) tab_bar_rect: Option<egui::Rect>,
|
||||||
pub(crate) menu_bar_stable_until: Option<std::time::Instant>,
|
pub(crate) menu_bar_stable_until: Option<std::time::Instant>,
|
||||||
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,
|
// Width calculation cache and state tracking
|
||||||
pub(crate) previous_content: String,
|
pub(crate) cached_width: Option<f32>,
|
||||||
pub(crate) previous_cursor_char_index: Option<usize>,
|
pub(crate) last_word_wrap: bool,
|
||||||
pub(crate) current_cursor_line: usize,
|
pub(crate) last_show_line_numbers: bool,
|
||||||
pub(crate) previous_cursor_line: usize,
|
pub(crate) last_font_size: f32,
|
||||||
pub(crate) font_settings_changed: bool,
|
pub(crate) last_line_side: bool,
|
||||||
pub(crate) text_needs_processing: bool,
|
pub(crate) last_viewport_width: f32,
|
||||||
pub(crate) should_select_current_match: bool,
|
// pub(crate) vim_mode: bool,
|
||||||
|
|
||||||
|
// Cursor tracking for smart scrolling
|
||||||
pub(crate) previous_cursor_position: Option<usize>,
|
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) {
|
|
||||||
&search_content[start..]
|
|
||||||
} else {
|
|
||||||
while start < search_content.len() && !search_content.is_char_boundary(start) {
|
|
||||||
start += 1;
|
|
||||||
}
|
|
||||||
if start >= search_content.len() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
&search_content[start..]
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(pos) = search_slice.find(&query) {
|
|
||||||
let absolute_pos = start + pos;
|
let absolute_pos = start + pos;
|
||||||
self.find_matches
|
self.find_matches
|
||||||
.push((absolute_pos, absolute_pos + query.len()));
|
.push((absolute_pos, absolute_pos + query.len()));
|
||||||
|
|
||||||
start = absolute_pos + 1;
|
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 {
|
|
||||||
if prev_index < self.find_matches.len() {
|
|
||||||
self.current_match_index = Some(prev_index);
|
|
||||||
} else {
|
|
||||||
self.current_match_index = Some(0);
|
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 { "" };
|
|
||||||
(
|
|
||||||
files,
|
|
||||||
"Unsaved Changes".to_string(),
|
"Unsaved Changes".to_string(),
|
||||||
format!("File{file_plural} with unsaved changes:"),
|
"You have unsaved changes.".to_string(),
|
||||||
"Quit Without Saving".to_string(),
|
"Quit Without Saving".to_string(),
|
||||||
action.to_owned(),
|
action.clone(),
|
||||||
)
|
),
|
||||||
}
|
|
||||||
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;
|
||||||
|
|||||||
@ -1,384 +1,63 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
use super::editor::{TextEditor, TextProcessingResult};
|
use super::editor::{TextEditor, TextProcessingResult};
|
||||||
use eframe::egui;
|
|
||||||
|
|
||||||
impl TextEditor {
|
impl TextEditor {
|
||||||
pub(crate) fn safe_slice_to_pos(content: &str, pos: usize) -> &str {
|
pub fn start_text_processing_thread(&mut self) {
|
||||||
let pos = pos.min(content.len());
|
let _processing_result = Arc::clone(&self.text_processing_result);
|
||||||
let mut boundary_pos = pos;
|
|
||||||
while boundary_pos > 0 && !content.is_char_boundary(boundary_pos) {
|
let handle = thread::Builder::new()
|
||||||
boundary_pos -= 1;
|
.name("TextProcessor".to_string())
|
||||||
|
.spawn(move || {
|
||||||
|
// Set thread priority to high (platform-specific)
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
unsafe {
|
||||||
|
let thread_id = libc::pthread_self();
|
||||||
|
let mut param: libc::sched_param = std::mem::zeroed();
|
||||||
|
param.sched_priority = 50;
|
||||||
|
let _ = libc::pthread_setschedparam(thread_id, libc::SCHED_OTHER, ¶m);
|
||||||
}
|
}
|
||||||
&content[..boundary_pos]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn process_text_for_rendering(&mut self, content: &str, ui: &egui::Ui) {
|
|
||||||
let line_count = content.bytes().filter(|&b| b == b'\n').count() + 1;
|
|
||||||
|
|
||||||
let lines: Vec<&str> = content.lines().collect();
|
|
||||||
|
|
||||||
if content.is_empty() {
|
|
||||||
self.update_processing_result(TextProcessingResult {
|
|
||||||
line_count: 1,
|
|
||||||
longest_line_index: 0,
|
|
||||||
longest_line_length: 0,
|
|
||||||
longest_line_pixel_width: 0.0,
|
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut longest_line_index = 0;
|
match handle {
|
||||||
let mut longest_line_length = 0;
|
Ok(h) => self.processing_thread_handle = Some(h),
|
||||||
|
Err(e) => eprintln!("Failed to start text processing thread: {}", e),
|
||||||
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() {
|
|
||||||
let char_count = line.chars().count();
|
|
||||||
if char_count > longest_line_length {
|
|
||||||
longest_line_length = char_count;
|
|
||||||
longest_line_index = index;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let font_id = self.get_font_id();
|
pub fn process_text_for_rendering(
|
||||||
let longest_line_pixel_width = if longest_line_length > 0 {
|
&mut self,
|
||||||
let longest_line_text = lines[longest_line_index];
|
content: &str,
|
||||||
ui.fonts(|fonts| {
|
available_width: f32,
|
||||||
fonts
|
) {
|
||||||
.layout(
|
let line_count = content.lines().count().max(1);
|
||||||
longest_line_text.to_string(),
|
|
||||||
font_id,
|
let visual_line_mapping = if self.word_wrap {
|
||||||
egui::Color32::WHITE,
|
// For now, simplified mapping - this could be moved to background thread
|
||||||
f32::INFINITY,
|
(1..=line_count).map(Some).collect()
|
||||||
)
|
|
||||||
.size()
|
|
||||||
.x
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
0.0
|
(1..=line_count).map(Some).collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = TextProcessingResult {
|
let result = TextProcessingResult {
|
||||||
line_count,
|
line_count,
|
||||||
longest_line_index,
|
visual_line_mapping,
|
||||||
longest_line_length,
|
max_line_length: available_width,
|
||||||
longest_line_pixel_width,
|
_processed_content: content.to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.update_processing_result(result);
|
if let Ok(mut processing_result) = self.text_processing_result.lock() {
|
||||||
}
|
*processing_result = result;
|
||||||
|
|
||||||
pub fn process_incremental_change(
|
|
||||||
&mut self,
|
|
||||||
old_content: &str,
|
|
||||||
new_content: &str,
|
|
||||||
old_cursor_pos: usize,
|
|
||||||
new_cursor_pos: usize,
|
|
||||||
ui: &egui::Ui,
|
|
||||||
) {
|
|
||||||
let line_change = self.calculate_cursor_line_change(
|
|
||||||
old_content,
|
|
||||||
new_content,
|
|
||||||
old_cursor_pos,
|
|
||||||
new_cursor_pos,
|
|
||||||
);
|
|
||||||
|
|
||||||
self.current_cursor_line = (self.current_cursor_line as isize + line_change) as usize;
|
|
||||||
|
|
||||||
if old_content.len() == new_content.len() {
|
|
||||||
self.handle_character_replacement(
|
|
||||||
old_content,
|
|
||||||
new_content,
|
|
||||||
old_cursor_pos,
|
|
||||||
new_cursor_pos,
|
|
||||||
ui,
|
|
||||||
);
|
|
||||||
} else if new_content.len() > old_content.len() {
|
|
||||||
self.handle_content_addition(
|
|
||||||
old_content,
|
|
||||||
new_content,
|
|
||||||
old_cursor_pos,
|
|
||||||
new_cursor_pos,
|
|
||||||
ui,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
self.handle_content_removal(
|
|
||||||
old_content,
|
|
||||||
new_content,
|
|
||||||
old_cursor_pos,
|
|
||||||
new_cursor_pos,
|
|
||||||
ui,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.previous_cursor_line = self.current_cursor_line;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn calculate_cursor_line_change(
|
|
||||||
&self,
|
|
||||||
old_content: &str,
|
|
||||||
new_content: &str,
|
|
||||||
old_cursor_pos: usize,
|
|
||||||
new_cursor_pos: usize,
|
|
||||||
) -> isize {
|
|
||||||
let old_newlines = Self::safe_slice_to_pos(old_content, old_cursor_pos)
|
|
||||||
.bytes()
|
|
||||||
.filter(|&b| b == b'\n')
|
|
||||||
.count();
|
|
||||||
|
|
||||||
let new_newlines = Self::safe_slice_to_pos(new_content, new_cursor_pos)
|
|
||||||
.bytes()
|
|
||||||
.filter(|&b| b == b'\n')
|
|
||||||
.count();
|
|
||||||
|
|
||||||
new_newlines as isize - old_newlines as isize
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_character_replacement(
|
|
||||||
&mut self,
|
|
||||||
_old_content: &str,
|
|
||||||
new_content: &str,
|
|
||||||
_old_cursor_pos: usize,
|
|
||||||
new_cursor_pos: usize,
|
|
||||||
ui: &egui::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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_content_addition(
|
|
||||||
&mut self,
|
|
||||||
old_content: &str,
|
|
||||||
new_content: &str,
|
|
||||||
_old_cursor_pos: usize,
|
|
||||||
new_cursor_pos: usize,
|
|
||||||
ui: &egui::Ui,
|
|
||||||
) {
|
|
||||||
let min_len = old_content.len().min(new_content.len());
|
|
||||||
let mut common_prefix = 0;
|
|
||||||
let mut common_suffix = 0;
|
|
||||||
for i in 0..min_len {
|
|
||||||
if old_content.as_bytes()[i] == new_content.as_bytes()[i] {
|
|
||||||
common_prefix += 1;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in 0..min_len - common_prefix {
|
|
||||||
let old_idx = old_content.len() - 1 - i;
|
|
||||||
let new_idx = new_content.len() - 1 - i;
|
|
||||||
if old_content.as_bytes()[old_idx] == new_content.as_bytes()[new_idx] {
|
|
||||||
common_suffix += 1;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let added_start = common_prefix;
|
|
||||||
let added_end = new_content.len() - common_suffix;
|
|
||||||
let added_text = &new_content[added_start..added_end];
|
|
||||||
let newlines_added = added_text.bytes().filter(|&b| b == b'\n').count();
|
|
||||||
|
|
||||||
if newlines_added > 0 {
|
|
||||||
let mut current_result = self.get_text_processing_result();
|
|
||||||
current_result.line_count += newlines_added;
|
|
||||||
|
|
||||||
let addition_start_line = Self::safe_slice_to_pos(old_content, added_start)
|
|
||||||
.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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_content_removal(
|
|
||||||
&mut self,
|
|
||||||
old_content: &str,
|
|
||||||
new_content: &str,
|
|
||||||
_old_cursor_pos: usize,
|
|
||||||
new_cursor_pos: usize,
|
|
||||||
ui: &egui::Ui,
|
|
||||||
) {
|
|
||||||
let min_len = old_content.len().min(new_content.len());
|
|
||||||
let mut common_prefix = 0;
|
|
||||||
let mut common_suffix = 0;
|
|
||||||
|
|
||||||
for i in 0..min_len {
|
|
||||||
if old_content.as_bytes()[i] == new_content.as_bytes()[i] {
|
|
||||||
common_prefix += 1;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in 0..min_len - common_prefix {
|
|
||||||
let old_idx = old_content.len() - 1 - i;
|
|
||||||
let new_idx = new_content.len() - 1 - i;
|
|
||||||
if old_content.as_bytes()[old_idx] == new_content.as_bytes()[new_idx] {
|
|
||||||
common_suffix += 1;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let removed_start = common_prefix;
|
|
||||||
let removed_end = old_content.len() - common_suffix;
|
|
||||||
let removed_text = &old_content[removed_start..removed_end];
|
|
||||||
let newlines_removed = removed_text.bytes().filter(|&b| b == b'\n').count();
|
|
||||||
|
|
||||||
if newlines_removed > 0 {
|
|
||||||
let mut current_result = self.get_text_processing_result();
|
|
||||||
current_result.line_count = current_result.line_count.saturating_sub(newlines_removed);
|
|
||||||
|
|
||||||
let removal_start_line = Self::safe_slice_to_pos(old_content, removed_start)
|
|
||||||
.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);
|
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let current_line = self.extract_current_line(new_content, new_cursor_pos);
|
|
||||||
let current_line_length = current_line.chars().count();
|
|
||||||
|
|
||||||
let current_result = self.get_text_processing_result();
|
|
||||||
if self.current_cursor_line == current_result.longest_line_index
|
|
||||||
&& current_line_length < current_result.longest_line_length
|
|
||||||
{
|
|
||||||
self.process_text_for_rendering(new_content, ui);
|
|
||||||
} else {
|
|
||||||
self.update_line_if_longer(
|
|
||||||
self.current_cursor_line,
|
|
||||||
¤t_line,
|
|
||||||
current_line_length,
|
|
||||||
ui,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extract_current_line(&self, content: &str, cursor_pos: usize) -> String {
|
|
||||||
let bytes = content.as_bytes();
|
|
||||||
let safe_cursor_pos = cursor_pos.min(bytes.len());
|
|
||||||
|
|
||||||
let mut line_start = safe_cursor_pos;
|
|
||||||
while line_start > 0 && bytes[line_start - 1] != b'\n' {
|
|
||||||
line_start -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut line_end = safe_cursor_pos;
|
|
||||||
while line_end < bytes.len() && bytes[line_end] != b'\n' {
|
|
||||||
line_end += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let line_start_boundary = line_start;
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_line_if_longer(
|
|
||||||
&mut self,
|
|
||||||
line_index: usize,
|
|
||||||
line_content: &str,
|
|
||||||
line_length: usize,
|
|
||||||
ui: &egui::Ui,
|
|
||||||
) {
|
|
||||||
let current_result = self.get_text_processing_result();
|
|
||||||
|
|
||||||
if line_length > current_result.longest_line_length {
|
|
||||||
let font_id = self.get_font_id();
|
|
||||||
let pixel_width = ui.fonts(|fonts| {
|
|
||||||
fonts
|
|
||||||
.layout(
|
|
||||||
line_content.to_string(),
|
|
||||||
font_id,
|
|
||||||
egui::Color32::WHITE,
|
|
||||||
f32::INFINITY,
|
|
||||||
)
|
|
||||||
.size()
|
|
||||||
.x
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = TextProcessingResult {
|
|
||||||
line_count: current_result.line_count,
|
|
||||||
longest_line_index: line_index,
|
|
||||||
longest_line_length: line_length,
|
|
||||||
longest_line_pixel_width: pixel_width,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.update_processing_result(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()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_processing_result(&self, result: TextProcessingResult) {
|
|
||||||
if let Ok(mut processing_result) = self.text_processing_result.lock() {
|
|
||||||
*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}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,18 +12,14 @@ impl TextEditor {
|
|||||||
pub fn get_title(&self) -> String {
|
pub fn get_title(&self) -> String {
|
||||||
if let Some(tab) = self.get_active_tab() {
|
if let Some(tab) = self.get_active_tab() {
|
||||||
let modified_indicator = if tab.is_modified { "*" } else { "" };
|
let modified_indicator = if tab.is_modified { "*" } else { "" };
|
||||||
format!(
|
format!("{}{} - C-Text", tab.title, modified_indicator)
|
||||||
"{}{} - {}",
|
|
||||||
tab.title,
|
|
||||||
modified_indicator,
|
|
||||||
env!("CARGO_PKG_NAME")
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
format!("{} - {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
|
"C-Text".to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_font_id(&self) -> egui::FontId {
|
/// Get the configured font ID based on the editor's font settings
|
||||||
|
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,
|
||||||
_ => egui::FontFamily::Proportional,
|
_ => egui::FontFamily::Proportional,
|
||||||
@ -42,17 +38,17 @@ impl TextEditor {
|
|||||||
_ => 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),
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.set_style(style);
|
ctx.set_style(style);
|
||||||
self.font_settings_changed = true;
|
|
||||||
self.save_config();
|
self.save_config();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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();
|
||||||
|
|
||||||
@ -64,9 +60,11 @@ impl TextEditor {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get line count from processing result
|
||||||
let processing_result = self.get_text_processing_result();
|
let processing_result = self.get_text_processing_result();
|
||||||
let line_count = processing_result.line_count;
|
let line_count = processing_result.line_count;
|
||||||
|
|
||||||
|
// Calculate base line number width
|
||||||
let font_id = self.get_font_id();
|
let font_id = self.get_font_id();
|
||||||
let line_count_digits = line_count.to_string().len();
|
let line_count_digits = line_count.to_string().len();
|
||||||
let sample_text = "9".repeat(line_count_digits);
|
let sample_text = "9".repeat(line_count_digits);
|
||||||
@ -77,10 +75,11 @@ impl TextEditor {
|
|||||||
.x
|
.x
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add padding based on line_side setting
|
||||||
let line_number_width = if self.line_side {
|
let line_number_width = if self.line_side {
|
||||||
base_line_number_width + 20.0
|
base_line_number_width + 20.0 // Extra padding when line numbers are on the side
|
||||||
} else {
|
} else {
|
||||||
base_line_number_width + 8.0
|
base_line_number_width + 8.0 // Minimal padding when line numbers are normal
|
||||||
};
|
};
|
||||||
|
|
||||||
// Separator space (7.0 for separator + 3.0 spacing = 10.0 total)
|
// Separator space (7.0 for separator + 3.0 spacing = 10.0 total)
|
||||||
@ -96,17 +95,68 @@ impl TextEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calculate the available width for non-word-wrapped content based on content analysis
|
||||||
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();
|
if let Some(active_tab) = self.get_active_tab() {
|
||||||
|
let content = &active_tab.content;
|
||||||
|
|
||||||
if processing_result.longest_line_length == 0 {
|
if content.is_empty() {
|
||||||
return self.calculate_editor_dimensions(ui).text_width;
|
return self.calculate_editor_dimensions(ui).text_width;
|
||||||
}
|
}
|
||||||
|
|
||||||
let longest_line_width =
|
// Find the longest line
|
||||||
processing_result.longest_line_pixel_width + (self.font_size * 2.0);
|
let longest_line = content
|
||||||
|
.lines()
|
||||||
|
.max_by_key(|line| line.chars().count())
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
if longest_line.is_empty() {
|
||||||
|
return self.calculate_editor_dimensions(ui).text_width;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the width needed for the longest line
|
||||||
|
let font_id = self.get_font_id();
|
||||||
|
let longest_line_width = ui.fonts(|fonts| {
|
||||||
|
fonts.layout(
|
||||||
|
longest_line.to_string(),
|
||||||
|
font_id,
|
||||||
|
egui::Color32::WHITE,
|
||||||
|
f32::INFINITY,
|
||||||
|
).size().x
|
||||||
|
}) + 20.0; // Add some padding
|
||||||
|
|
||||||
|
// Return the larger of the calculated width or minimum available width
|
||||||
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)
|
||||||
|
} else {
|
||||||
|
self.calculate_editor_dimensions(ui).text_width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if width calculation needs to be performed based on parameter changes
|
||||||
|
pub fn needs_width_calculation(&self, ctx: &egui::Context) -> bool {
|
||||||
|
let current_viewport_width = ctx.available_rect().width();
|
||||||
|
|
||||||
|
self.cached_width.is_none() ||
|
||||||
|
self.word_wrap != self.last_word_wrap ||
|
||||||
|
self.show_line_numbers != self.last_show_line_numbers ||
|
||||||
|
(self.font_size - self.last_font_size).abs() > 0.1 ||
|
||||||
|
self.line_side != self.last_line_side ||
|
||||||
|
(current_viewport_width - self.last_viewport_width).abs() > 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the cached width calculation state
|
||||||
|
pub fn update_width_calculation_state(&mut self, ctx: &egui::Context, width: f32) {
|
||||||
|
self.cached_width = Some(width);
|
||||||
|
self.last_word_wrap = self.word_wrap;
|
||||||
|
self.last_show_line_numbers = self.show_line_numbers;
|
||||||
|
self.last_font_size = self.font_size;
|
||||||
|
self.last_line_side = self.line_side;
|
||||||
|
self.last_viewport_width = ctx.available_rect().width();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get cached width if available, otherwise return None to indicate calculation is needed
|
||||||
|
pub fn get_cached_width(&self) -> Option<f32> {
|
||||||
|
self.cached_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,
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
131
src/app/theme.rs
131
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 {
|
||||||
@ -82,7 +77,7 @@ fn get_pywal_colors() -> Option<egui::Visuals> {
|
|||||||
let fg = parse_color(colors.get(7).unwrap_or(&colors[0]))?;
|
let fg = parse_color(colors.get(7).unwrap_or(&colors[0]))?;
|
||||||
let bg_alt = parse_color(colors.get(8).unwrap_or(&colors[0]))?;
|
let bg_alt = parse_color(colors.get(8).unwrap_or(&colors[0]))?;
|
||||||
let accent = parse_color(colors.get(1).unwrap_or(&colors[0]))?;
|
let accent = parse_color(colors.get(1).unwrap_or(&colors[0]))?;
|
||||||
let _secondary = parse_color(colors.get(2).unwrap_or(&colors[0]))?;
|
let secondary = parse_color(colors.get(2).unwrap_or(&colors[0]))?;
|
||||||
|
|
||||||
let mut visuals = if is_dark_color(bg) {
|
let mut visuals = if is_dark_color(bg) {
|
||||||
egui::Visuals::dark()
|
egui::Visuals::dark()
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
|
|||||||
196
src/io.rs
196
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,86 +24,35 @@ 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) => {
|
||||||
eprintln!("Failed to open file: {err}");
|
eprintln!("Failed to open file: {}", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,20 +72,16 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
27
src/main.rs
27
src/main.rs
@ -1,43 +1,26 @@
|
|||||||
#![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;
|
||||||
mod ui;
|
mod ui;
|
||||||
use app::{config::Config, TextEditor};
|
use app::{TextEditor, config::Config};
|
||||||
|
|
||||||
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])
|
||||||
.with_title("ced")
|
.with_title("C-Ext")
|
||||||
.with_app_id("io.lampnet.ced"),
|
.with_app_id("io.lampnet.c-ext"),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let config = Config::load();
|
let config = Config::load();
|
||||||
|
|
||||||
eframe::run_native(
|
eframe::run_native(
|
||||||
"ced",
|
"C-Ext",
|
||||||
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};
|
||||||
@ -17,7 +14,7 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
let line_side = app.line_side;
|
let line_side = app.line_side;
|
||||||
let font_size = app.font_size;
|
let font_size = app.font_size;
|
||||||
|
|
||||||
let _output = egui::CentralPanel::default()
|
egui::CentralPanel::default()
|
||||||
.frame(egui::Frame::NONE)
|
.frame(egui::Frame::NONE)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
let bg_color = ui.visuals().extreme_bg_color;
|
let bg_color = ui.visuals().extreme_bg_color;
|
||||||
@ -26,20 +23,11 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
let editor_height = panel_rect.height();
|
let editor_height = panel_rect.height();
|
||||||
|
|
||||||
if !show_line_numbers || app.get_active_tab().is_none() {
|
if !show_line_numbers || app.get_active_tab().is_none() {
|
||||||
let _scroll_response =
|
|
||||||
egui::ScrollArea::vertical()
|
egui::ScrollArea::vertical()
|
||||||
.auto_shrink([false; 2])
|
.auto_shrink([false; 2])
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
let full_rect = ui.available_rect_before_wrap();
|
|
||||||
let context_response =
|
|
||||||
ui.allocate_response(full_rect.size(), egui::Sense::click());
|
|
||||||
|
|
||||||
ui.scope_builder(egui::UiBuilder::new().max_rect(full_rect), |ui| {
|
|
||||||
editor_view_ui(ui, app);
|
editor_view_ui(ui, app);
|
||||||
});
|
});
|
||||||
|
|
||||||
handle_empty(ui, app, &context_response);
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,146 +63,50 @@ 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 {
|
||||||
let text_editor_width =
|
// Line numbers on the right
|
||||||
editor_dimensions.text_width + editor_dimensions.total_reserved_width;
|
let text_editor_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| {
|
|
||||||
let full_rect = ui.available_rect_before_wrap();
|
|
||||||
let context_response = ui.allocate_response(
|
|
||||||
full_rect.size(),
|
|
||||||
egui::Sense::click(),
|
|
||||||
);
|
|
||||||
|
|
||||||
ui.scope_builder(
|
|
||||||
egui::UiBuilder::new().max_rect(full_rect),
|
|
||||||
|ui| {
|
|ui| {
|
||||||
editor_view_ui(ui, app);
|
editor_view_ui(ui, app);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
handle_empty(ui, app, &context_response);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
separator_widget(ui);
|
separator_widget(ui);
|
||||||
line_numbers_widget(ui);
|
line_numbers_widget(ui);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let text_editor_width =
|
// Line numbers on the left
|
||||||
editor_dimensions.text_width + editor_dimensions.total_reserved_width;
|
let text_editor_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| {
|
||||||
line_numbers_widget(ui);
|
line_numbers_widget(ui);
|
||||||
separator_widget(ui);
|
separator_widget(ui);
|
||||||
|
|
||||||
let editor_area = ui.available_rect_before_wrap();
|
|
||||||
let context_response =
|
|
||||||
ui.allocate_response(editor_area.size(), egui::Sense::click());
|
|
||||||
|
|
||||||
ui.scope_builder(
|
|
||||||
egui::UiBuilder::new().max_rect(editor_area),
|
|
||||||
|ui| {
|
|
||||||
editor_view_ui(ui, app);
|
editor_view_ui(ui, app);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
handle_empty(ui, app, &context_response);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_empty(_ui: &mut egui::Ui, app: &mut TextEditor, context_response: &egui::Response) {
|
|
||||||
if context_response.clicked() {
|
|
||||||
let text_edit_id = egui::Id::new("main_text_editor");
|
|
||||||
if let Some(mut state) = egui::TextEdit::load_state(_ui.ctx(), text_edit_id) {
|
|
||||||
if let Some(active_tab) = app.get_active_tab() {
|
|
||||||
let text_len = active_tab.content.len();
|
|
||||||
let cursor_pos = egui::text::CCursor::new(text_len);
|
|
||||||
state
|
|
||||||
.cursor
|
|
||||||
.set_char_range(Some(egui::text::CCursorRange::one(cursor_pos)));
|
|
||||||
egui::TextEdit::store_state(_ui.ctx(), text_edit_id, state);
|
|
||||||
|
|
||||||
_ui.ctx().memory_mut(|mem| {
|
|
||||||
mem.request_focus(text_edit_id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
context_response.context_menu(|ui| {
|
|
||||||
let text_len = app.get_active_tab().unwrap().content.len();
|
|
||||||
let reset_zoom_key = egui::Id::new("editor_reset_zoom");
|
|
||||||
|
|
||||||
if ui.button("Cut").clicked() {
|
|
||||||
ui.ctx()
|
|
||||||
.send_viewport_cmd(egui::ViewportCommand::RequestCut);
|
|
||||||
ui.close_kind(UiKind::Menu);
|
|
||||||
}
|
|
||||||
if ui.button("Copy").clicked() {
|
|
||||||
ui.ctx()
|
|
||||||
.send_viewport_cmd(egui::ViewportCommand::RequestCopy);
|
|
||||||
ui.close_kind(UiKind::Menu);
|
|
||||||
}
|
|
||||||
if ui.button("Paste").clicked() {
|
|
||||||
ui.ctx()
|
|
||||||
.send_viewport_cmd(egui::ViewportCommand::RequestPaste);
|
|
||||||
ui.close_kind(UiKind::Menu);
|
|
||||||
}
|
|
||||||
if ui.button("Delete").clicked() {
|
|
||||||
ui.ctx().input_mut(|i| {
|
|
||||||
i.events.push(egui::Event::Key {
|
|
||||||
key: egui::Key::Delete,
|
|
||||||
physical_key: None,
|
|
||||||
pressed: true,
|
|
||||||
repeat: false,
|
|
||||||
modifiers: egui::Modifiers::NONE,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
ui.close_kind(UiKind::Menu);
|
|
||||||
}
|
|
||||||
if ui.button("Select All").clicked() {
|
|
||||||
let text_edit_id = egui::Id::new("main_text_editor");
|
|
||||||
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
|
||||||
let select_all_range = egui::text::CCursorRange::two(
|
|
||||||
egui::text::CCursor::new(0),
|
|
||||||
egui::text::CCursor::new(text_len),
|
|
||||||
);
|
|
||||||
state.cursor.set_char_range(Some(select_all_range));
|
|
||||||
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
|
||||||
}
|
|
||||||
ui.close_kind(UiKind::Menu);
|
|
||||||
}
|
|
||||||
ui.separator();
|
|
||||||
if ui.button("Reset Zoom").clicked() {
|
|
||||||
ui.ctx().memory_mut(|mem| {
|
|
||||||
mem.data.insert_temp(reset_zoom_key, true);
|
|
||||||
});
|
|
||||||
ui.close_kind(UiKind::Menu);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,26 +1,28 @@
|
|||||||
use crate::app::TextEditor;
|
use crate::app::TextEditor;
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use egui_extras::syntax_highlighting::{self};
|
|
||||||
|
|
||||||
use super::find_highlight;
|
use super::find_highlight::draw_find_highlight;
|
||||||
|
|
||||||
pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::Response {
|
pub(super) fn editor_view(
|
||||||
let _current_match_position = app.get_current_match_position();
|
ui: &mut egui::Ui,
|
||||||
|
app: &mut TextEditor,
|
||||||
|
) -> (egui::Response, Option<egui::Rect>) {
|
||||||
|
let current_match_position = app.get_current_match_position();
|
||||||
let show_find = app.show_find;
|
let show_find = app.show_find;
|
||||||
let _prev_show_find = app.prev_show_find;
|
let prev_show_find = app.prev_show_find;
|
||||||
let show_preferences = app.show_preferences;
|
let show_preferences = app.show_preferences;
|
||||||
let show_about = app.show_about;
|
let show_about = app.show_about;
|
||||||
let show_shortcuts = app.show_shortcuts;
|
let show_shortcuts = app.show_shortcuts;
|
||||||
let word_wrap = app.word_wrap;
|
let word_wrap = app.word_wrap;
|
||||||
let font_size = app.font_size;
|
let font_size = app.font_size;
|
||||||
let font_id = app.get_font_id();
|
|
||||||
let syntax_highlighting_enabled = app.syntax_highlighting;
|
|
||||||
|
|
||||||
|
// Check if reset zoom was requested in previous frame
|
||||||
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.ctx().memory_mut(|mem| {
|
||||||
.ctx()
|
mem.data.get_temp::<bool>(reset_zoom_key).unwrap_or(false)
|
||||||
.memory_mut(|mem| mem.data.get_temp::<bool>(reset_zoom_key).unwrap_or(false));
|
});
|
||||||
|
|
||||||
|
// Reset zoom if requested
|
||||||
if should_reset_zoom {
|
if should_reset_zoom {
|
||||||
app.zoom_factor = 1.0;
|
app.zoom_factor = 1.0;
|
||||||
ui.ctx().set_zoom_factor(1.0);
|
ui.ctx().set_zoom_factor(1.0);
|
||||||
@ -29,204 +31,93 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let estimated_width = if !word_wrap {
|
if let Some(active_tab) = app.get_active_tab_mut() {
|
||||||
app.calculate_content_based_width(ui)
|
|
||||||
} else {
|
|
||||||
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 {
|
|
||||||
return ui.label("No file open, how did you get here?");
|
|
||||||
};
|
|
||||||
|
|
||||||
let bg_color = ui.visuals().extreme_bg_color;
|
let bg_color = ui.visuals().extreme_bg_color;
|
||||||
let editor_rect = ui.available_rect_before_wrap();
|
let editor_rect = ui.available_rect_before_wrap();
|
||||||
ui.painter().rect_filled(editor_rect, 0.0, bg_color);
|
ui.painter().rect_filled(editor_rect, 0.0, bg_color);
|
||||||
|
|
||||||
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 {
|
let desired_width = if word_wrap {
|
||||||
ui.available_width()
|
ui.available_width()
|
||||||
} else {
|
} else {
|
||||||
f32::INFINITY
|
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 {
|
|
||||||
ui.available_width()
|
|
||||||
} else {
|
|
||||||
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 = text_edit.show(ui);
|
||||||
text_edit.show(ui)
|
|
||||||
} else {
|
// Store text length for context menu
|
||||||
egui::ScrollArea::horizontal()
|
let text_len = active_tab.content.len();
|
||||||
.auto_shrink([false; 2])
|
|
||||||
.show(ui, |ui| {
|
// Right-click context menu
|
||||||
ui.allocate_ui_with_layout(
|
output.response.context_menu(|ui| {
|
||||||
egui::Vec2::new(estimated_width, ui.available_height()),
|
if ui.button("Cut").clicked() {
|
||||||
egui::Layout::left_to_right(egui::Align::TOP),
|
ui.ctx().input_mut(|i| i.events.push(egui::Event::Cut));
|
||||||
|ui| text_edit.show(ui),
|
ui.close_menu();
|
||||||
)
|
}
|
||||||
|
if ui.button("Copy").clicked() {
|
||||||
|
ui.ctx().input_mut(|i| i.events.push(egui::Event::Copy));
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
if ui.button("Paste").clicked() {
|
||||||
|
ui.ctx()
|
||||||
|
.send_viewport_cmd(egui::ViewportCommand::RequestPaste);
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
if ui.button("Delete").clicked() {
|
||||||
|
ui.ctx().input_mut(|i| {
|
||||||
|
i.events.push(egui::Event::Key {
|
||||||
|
key: egui::Key::Delete,
|
||||||
|
physical_key: None,
|
||||||
|
pressed: true,
|
||||||
|
repeat: false,
|
||||||
|
modifiers: egui::Modifiers::NONE,
|
||||||
})
|
})
|
||||||
.inner
|
});
|
||||||
.inner
|
ui.close_menu();
|
||||||
};
|
|
||||||
|
|
||||||
let content_changed = output.response.changed();
|
|
||||||
let content_for_processing = if content_changed {
|
|
||||||
active_tab.update_modified_state();
|
|
||||||
Some(active_tab.content.to_owned())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if content_changed {
|
|
||||||
if let Err(e) = app.save_state_cache() {
|
|
||||||
eprintln!("Failed to save state cache: {e}");
|
|
||||||
}
|
}
|
||||||
}
|
if ui.button("Select All").clicked() {
|
||||||
|
let text_edit_id = egui::Id::new("main_text_editor");
|
||||||
if content_changed && app.show_find && !app.find_query.is_empty() {
|
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
||||||
app.update_find_matches();
|
let select_all_range = egui::text::CCursorRange::two(
|
||||||
}
|
egui::text::CCursor::new(0),
|
||||||
|
egui::text::CCursor::new(text_len),
|
||||||
let current_cursor_pos = output
|
|
||||||
.state
|
|
||||||
.cursor
|
|
||||||
.char_range()
|
|
||||||
.map(|range| range.primary.index);
|
|
||||||
|
|
||||||
if let Some(content) = content_for_processing {
|
|
||||||
let previous_content = app.previous_content.to_owned();
|
|
||||||
let previous_cursor_pos = app.previous_cursor_char_index;
|
|
||||||
|
|
||||||
if !previous_content.is_empty() {
|
|
||||||
if let (Some(prev_cursor_pos), Some(curr_cursor_pos)) =
|
|
||||||
(previous_cursor_pos, current_cursor_pos)
|
|
||||||
{
|
|
||||||
app.process_incremental_change(
|
|
||||||
&previous_content,
|
|
||||||
&content,
|
|
||||||
prev_cursor_pos,
|
|
||||||
curr_cursor_pos,
|
|
||||||
ui,
|
|
||||||
);
|
);
|
||||||
|
state.cursor.set_char_range(Some(select_all_range));
|
||||||
|
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
||||||
}
|
}
|
||||||
} else {
|
ui.close_menu();
|
||||||
app.process_text_for_rendering(&content, ui);
|
|
||||||
}
|
}
|
||||||
|
ui.separator();
|
||||||
|
if ui.button("Reset Zoom").clicked() {
|
||||||
|
ui.ctx().memory_mut(|mem| {
|
||||||
|
mem.data.insert_temp(reset_zoom_key, true);
|
||||||
|
});
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.previous_content = content.to_owned();
|
let cursor_rect = if let Some(cursor_range) = output.state.cursor.char_range() {
|
||||||
app.previous_cursor_char_index = current_cursor_pos;
|
let cursor_pos = cursor_range.primary.index;
|
||||||
}
|
|
||||||
|
|
||||||
if app.font_settings_changed || app.text_needs_processing {
|
|
||||||
if let Some(active_tab) = app.get_active_tab() {
|
|
||||||
let content = active_tab.content.to_owned();
|
|
||||||
app.process_text_for_rendering(&content, ui);
|
|
||||||
}
|
|
||||||
app.font_settings_changed = false;
|
|
||||||
app.text_needs_processing = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !word_wrap {
|
|
||||||
if let Some(cursor_pos) = current_cursor_pos {
|
|
||||||
let cursor_moved = Some(cursor_pos) != app.previous_cursor_position;
|
|
||||||
let text_changed = output.response.changed();
|
|
||||||
|
|
||||||
if cursor_moved || text_changed {
|
|
||||||
if let Some(active_tab) = app.get_active_tab() {
|
|
||||||
let content = &active_tab.content;
|
let content = &active_tab.content;
|
||||||
let cursor_line = content
|
|
||||||
.char_indices()
|
let text_up_to_cursor = &content[..cursor_pos.min(content.len())];
|
||||||
.take_while(|(byte_pos, _)| *byte_pos < cursor_pos)
|
let cursor_line = text_up_to_cursor.chars().filter(|&c| c == '\n').count();
|
||||||
.filter(|(_, ch)| *ch == '\n')
|
|
||||||
.count();
|
|
||||||
|
|
||||||
let font_id = ui
|
let font_id = ui
|
||||||
.style()
|
.style()
|
||||||
.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);
|
||||||
@ -234,17 +125,44 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
|||||||
egui::pos2(output.response.rect.left(), y_pos),
|
egui::pos2(output.response.rect.left(), y_pos),
|
||||||
egui::vec2(2.0, line_height),
|
egui::vec2(2.0, line_height),
|
||||||
);
|
);
|
||||||
|
Some(cursor_rect)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let visible_area = ui.clip_rect();
|
if !show_find && prev_show_find {
|
||||||
if !visible_area.intersects(cursor_rect) {
|
if let Some((start_pos, end_pos)) = current_match_position {
|
||||||
ui.scroll_to_rect(cursor_rect, Some(egui::Align::Center));
|
let text_edit_id = egui::Id::new("main_text_editor");
|
||||||
|
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
||||||
|
let cursor_range = egui::text::CCursorRange::two(
|
||||||
|
egui::text::CCursor::new(start_pos),
|
||||||
|
egui::text::CCursor::new(end_pos),
|
||||||
|
);
|
||||||
|
state.cursor.set_char_range(Some(cursor_range));
|
||||||
|
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
app.previous_cursor_position = Some(cursor_pos);
|
|
||||||
|
if show_find {
|
||||||
|
if let Some((start_pos, end_pos)) = current_match_position {
|
||||||
|
draw_find_highlight(
|
||||||
|
ui,
|
||||||
|
&active_tab.content,
|
||||||
|
start_pos,
|
||||||
|
end_pos,
|
||||||
|
output.response.rect,
|
||||||
|
font_size,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if output.response.changed() {
|
||||||
|
active_tab.update_modified_state();
|
||||||
|
app.find_matches.clear();
|
||||||
|
app.current_match_index = None;
|
||||||
|
}
|
||||||
|
|
||||||
if !output.response.has_focus()
|
if !output.response.has_focus()
|
||||||
&& !show_preferences
|
&& !show_preferences
|
||||||
&& !show_about
|
&& !show_about
|
||||||
@ -253,6 +171,52 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
|||||||
{
|
{
|
||||||
output.response.request_focus();
|
output.response.request_focus();
|
||||||
}
|
}
|
||||||
|
(output.response, cursor_rect)
|
||||||
output.response
|
} else {
|
||||||
|
(ui.label("No file open, how did you get here?"), None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) {
|
||||||
|
let word_wrap = app.word_wrap;
|
||||||
|
|
||||||
|
if word_wrap {
|
||||||
|
let (_response, _cursor_rect) = editor_view(ui, app);
|
||||||
|
} else {
|
||||||
|
let estimated_width = app.calculate_content_based_width(ui);
|
||||||
|
let output = egui::ScrollArea::horizontal()
|
||||||
|
.auto_shrink([false; 2])
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.allocate_ui_with_layout(
|
||||||
|
egui::Vec2::new(estimated_width, ui.available_height()),
|
||||||
|
egui::Layout::left_to_right(egui::Align::TOP),
|
||||||
|
|ui| editor_view(ui, app),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let editor_response = &output.inner.inner.0;
|
||||||
|
if let Some(cursor_rect) = output.inner.inner.1 {
|
||||||
|
let text_edit_id = egui::Id::new("main_text_editor");
|
||||||
|
let current_cursor_pos =
|
||||||
|
if let Some(state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
||||||
|
state.cursor.char_range().map(|range| range.primary.index)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let cursor_moved = current_cursor_pos != app.previous_cursor_position;
|
||||||
|
let text_changed = editor_response.changed();
|
||||||
|
let should_scroll = (cursor_moved || text_changed)
|
||||||
|
&& {
|
||||||
|
let visible_area = ui.clip_rect();
|
||||||
|
!visible_area.intersects(cursor_rect)
|
||||||
|
};
|
||||||
|
|
||||||
|
if should_scroll {
|
||||||
|
ui.scroll_to_rect(cursor_rect, Some(egui::Align::Center));
|
||||||
|
}
|
||||||
|
|
||||||
|
app.previous_cursor_position = current_cursor_pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,17 +51,17 @@ 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_text = &content[start_pos..end_pos.min(content.len())];
|
||||||
|
|
||||||
let match_width = ui.fonts(|fonts| {
|
let match_width = ui.fonts(|fonts| {
|
||||||
fonts
|
fonts
|
||||||
.layout(
|
.layout(
|
||||||
match_text.to_string(),
|
match_text.to_string(),
|
||||||
font_id.to_owned(),
|
font_id.clone(),
|
||||||
ui.visuals().text_color(),
|
ui.visuals().text_color(),
|
||||||
f32::INFINITY,
|
f32::INFINITY,
|
||||||
)
|
)
|
||||||
@ -111,12 +74,10 @@ fn draw_single_highlight(
|
|||||||
egui::vec2(match_width, line_height),
|
egui::vec2(match_width, line_height),
|
||||||
);
|
);
|
||||||
|
|
||||||
let highlight_color = if is_current_match {
|
ui.painter().rect_filled(
|
||||||
ui.visuals().selection.bg_fill
|
highlight_rect,
|
||||||
} else {
|
0.0,
|
||||||
ui.visuals().selection.bg_fill.gamma_multiply(0.6)
|
ui.visuals().selection.bg_fill,
|
||||||
};
|
);
|
||||||
|
}
|
||||||
let painter = ui.painter();
|
|
||||||
painter.rect_filled(highlight_rect, 0.0, highlight_color);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
)
|
)
|
||||||
@ -86,7 +86,8 @@ pub(super) fn render_line_numbers(
|
|||||||
let bg_color = ui.visuals().extreme_bg_color;
|
let bg_color = ui.visuals().extreme_bg_color;
|
||||||
|
|
||||||
let line_numbers_rect = ui.available_rect_before_wrap();
|
let line_numbers_rect = ui.available_rect_before_wrap();
|
||||||
ui.painter().rect_filled(line_numbers_rect, 0.0, bg_color);
|
ui.painter()
|
||||||
|
.rect_filled(line_numbers_rect, 0.0, bg_color);
|
||||||
|
|
||||||
let font_id = egui::FontId::monospace(font_size);
|
let font_id = egui::FontId::monospace(font_size);
|
||||||
let line_count_width = line_count.to_string().len();
|
let line_count_width = line_count.to_string().len();
|
||||||
@ -100,7 +101,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 +110,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_space(8.0);
|
||||||
ui.add_enabled_ui(replace_all_enabled, |ui| {
|
|
||||||
if ui.button("Replace All").clicked() {
|
|
||||||
app.replace_all(ctx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.add_space(MEDIUM);
|
|
||||||
|
|
||||||
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();
|
||||||
@ -12,12 +11,12 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
|
|
||||||
let should_show_menubar = !app.auto_hide_toolbar || {
|
let should_show_menubar = !app.auto_hide_toolbar || {
|
||||||
if app.menu_interaction_active {
|
if app.menu_interaction_active {
|
||||||
app.menu_bar_stable_until = Some(now + std::time::Duration::from_millis(16));
|
app.menu_bar_stable_until = Some(now + std::time::Duration::from_millis(500));
|
||||||
true
|
true
|
||||||
} else if should_stay_stable {
|
} else if should_stay_stable {
|
||||||
true
|
true
|
||||||
} else if let Some(pointer_pos) = ctx.pointer_hover_pos() {
|
} else if let Some(pointer_pos) = ctx.pointer_hover_pos() {
|
||||||
let in_menu_trigger_area = pointer_pos.y < 5.0;
|
let in_menu_trigger_area = pointer_pos.y < 10.0;
|
||||||
|
|
||||||
if in_menu_trigger_area {
|
if in_menu_trigger_area {
|
||||||
app.menu_bar_stable_until = Some(now + std::time::Duration::from_millis(300));
|
app.menu_bar_stable_until = Some(now + std::time::Duration::from_millis(300));
|
||||||
@ -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_menu();
|
||||||
ui.close_kind(UiKind::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,57 +151,46 @@ 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_menu();
|
||||||
ui.close_kind(UiKind::Menu);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.menu_button("View", |ui| {
|
ui.menu_button("View", |ui| {
|
||||||
app.menu_interaction_active = true;
|
app.menu_interaction_active = true;
|
||||||
if ui
|
if ui
|
||||||
.checkbox(&mut app.show_line_numbers, "Show Line Numbers")
|
.checkbox(&mut app.show_line_numbers, "Toggle Line Numbers")
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
app.save_config();
|
app.save_config();
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
if ui
|
if ui
|
||||||
.checkbox(&mut app.syntax_highlighting, "Syntax Highlighting")
|
.checkbox(&mut app.word_wrap, "Toggle Word Wrap")
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
app.save_config();
|
app.save_config();
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
|
||||||
if ui.checkbox(&mut app.word_wrap, "Word Wrap").clicked() {
|
|
||||||
app.save_config();
|
|
||||||
ui.close_kind(UiKind::Menu);
|
|
||||||
}
|
|
||||||
if ui.checkbox(&mut app.hide_tab_bar, "Hide Tab Bar").clicked() {
|
|
||||||
app.save_config();
|
|
||||||
ui.close_kind(UiKind::Menu);
|
|
||||||
}
|
}
|
||||||
if ui
|
if ui
|
||||||
.checkbox(&mut app.auto_hide_toolbar, "Auto Hide Toolbar")
|
.checkbox(&mut app.auto_hide_toolbar, "Auto Hide Toolbar")
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
app.save_config();
|
app.save_config();
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
@ -213,7 +198,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 +218,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 +231,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 +240,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,49 +258,13 @@ 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 {
|
|
||||||
let tab_title = if let Some(tab) = app.get_active_tab() {
|
|
||||||
tab.get_display_title()
|
|
||||||
} else {
|
|
||||||
let empty_tab = crate::app::tab::Tab::new_empty(1);
|
|
||||||
empty_tab.get_display_title()
|
|
||||||
};
|
|
||||||
|
|
||||||
let window_width = ctx.screen_rect().width();
|
|
||||||
let font_id = ui.style().text_styles[&egui::TextStyle::Body].to_owned();
|
|
||||||
|
|
||||||
let text_galley = ui.fonts(|fonts| {
|
|
||||||
fonts.layout_job(egui::text::LayoutJob::simple_singleline(
|
|
||||||
tab_title,
|
|
||||||
font_id,
|
|
||||||
ui.style().visuals.text_color(),
|
|
||||||
))
|
|
||||||
});
|
|
||||||
|
|
||||||
let text_width = text_galley.size().x;
|
|
||||||
let text_height = text_galley.size().y;
|
|
||||||
|
|
||||||
let window_center_x = window_width / 2.0;
|
|
||||||
let text_x = (window_center_x - text_width / 2.0).max(0.0);
|
|
||||||
|
|
||||||
let cursor_pos = ui.cursor().left_top();
|
|
||||||
|
|
||||||
ui.painter().galley(
|
|
||||||
egui::pos2(text_x, cursor_pos.y),
|
|
||||||
text_galley,
|
|
||||||
ui.style().visuals.text_color(),
|
|
||||||
);
|
|
||||||
|
|
||||||
ui.allocate_exact_size(egui::vec2(0.0, text_height), egui::Sense::hover());
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,91 +14,23 @@ 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(SMALL);
|
ui.add_space(5.0);
|
||||||
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)
|
||||||
@ -128,25 +57,30 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
app.apply_font_settings(ctx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Font Size:");
|
||||||
|
ui.add_space(5.0);
|
||||||
|
|
||||||
if app.font_size_input.is_none() {
|
if app.font_size_input.is_none() {
|
||||||
app.font_size_input = Some(app.font_size.to_string());
|
app.font_size_input = Some(app.font_size.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut font_size_text = app
|
let mut font_size_text = app.font_size_input.as_ref().unwrap().clone();
|
||||||
.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(
|
let response = ui.add(
|
||||||
egui::TextEdit::singleline(&mut font_size_text)
|
egui::TextEdit::singleline(&mut font_size_text)
|
||||||
.desired_width(FONT_SIZE_INPUT_WIDTH)
|
.desired_width(50.0)
|
||||||
.hint_text(DEFAULT_FONT_SIZE_STR)
|
.hint_text("14")
|
||||||
.id(egui::Id::new("font_size_input")),
|
.id(egui::Id::new("font_size_input")),
|
||||||
);
|
);
|
||||||
|
|
||||||
app.font_size_input = Some(font_size_text.to_owned());
|
app.font_size_input = Some(font_size_text.clone());
|
||||||
|
|
||||||
if response.clicked() {
|
if response.clicked() {
|
||||||
response.request_focus();
|
response.request_focus();
|
||||||
@ -156,7 +90,7 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
|
|
||||||
if response.lost_focus() {
|
if response.lost_focus() {
|
||||||
if let Ok(new_size) = font_size_text.parse::<f32>() {
|
if let Ok(new_size) = font_size_text.parse::<f32>() {
|
||||||
let clamped_size = new_size.clamp(MIN_FONT_SIZE, MAX_FONT_SIZE);
|
let clamped_size = new_size.clamp(8.0, 32.0);
|
||||||
if (app.font_size - clamped_size).abs() > 0.1 {
|
if (app.font_size - clamped_size).abs() > 0.1 {
|
||||||
app.font_size = clamped_size;
|
app.font_size = clamped_size;
|
||||||
app.apply_font_settings(ctx);
|
app.apply_font_settings(ctx);
|
||||||
@ -164,26 +98,22 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
}
|
}
|
||||||
app.font_size_input = None;
|
app.font_size_input = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
if changed {
|
|
||||||
app.apply_font_settings(ctx);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(MEDIUM);
|
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,
|
||||||
@ -193,27 +123,22 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
ui.label(
|
ui.label(
|
||||||
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.clone()),
|
||||||
)
|
|
||||||
.font(preview_font.to_owned()),
|
|
||||||
);
|
);
|
||||||
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(
|
|
||||||
egui::RichText::new("1234567890 !@#$%^&*()")
|
|
||||||
.font(preview_font.to_owned()),
|
|
||||||
);
|
);
|
||||||
|
ui.label(egui::RichText::new("1234567890 !@#$%^&*()").font(preview_font));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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,41 @@
|
|||||||
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(UI_TEXT_SIZE),
|
egui::RichText::new("Ctrl + Shift + L: Change Line Number Side").size(14.0),
|
||||||
);
|
);
|
||||||
ui.label(egui::RichText::new("Ctrl + K: Toggle Word Wrap").size(UI_TEXT_SIZE));
|
ui.label(egui::RichText::new("Ctrl + K: Toggle Word Wrap").size(14.0));
|
||||||
ui.label(egui::RichText::new("Ctrl + H: Toggle Auto Hide Toolbar").size(UI_TEXT_SIZE));
|
ui.label(egui::RichText::new("Ctrl + H: Toggle Auto Hide Toolbar").size(14.0));
|
||||||
ui.label(egui::RichText::new("Ctrl + P: Preferences").size(UI_TEXT_SIZE));
|
ui.label(egui::RichText::new("Ctrl + P: Preferences").size(14.0));
|
||||||
ui.label(egui::RichText::new("Ctrl + =/-: Increase/Decrease Font Size").size(UI_TEXT_SIZE));
|
ui.label(egui::RichText::new("Ctrl + =/-: Increase/Decrease Font Size").size(14.0));
|
||||||
ui.label(egui::RichText::new("Ctrl + Shift + =/-: Zoom In/Out").size(UI_TEXT_SIZE));
|
ui.label(egui::RichText::new("Ctrl + Shift + =/-: Zoom In/Out").size(14.0));
|
||||||
|
|
||||||
// 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 +44,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 +52,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).min(400.0).max(300.0);
|
||||||
let window_height =
|
let window_height = (screen_rect.height() * 0.7).min(500.0).max(250.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 +85,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,14 +3,9 @@ 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()
|
|
||||||
.auto_shrink([false, true])
|
|
||||||
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden)
|
|
||||||
.scroll_source(egui::scroll_area::ScrollSource::DRAG)
|
|
||||||
.show(ui, |ui| {
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
let mut tab_to_close_unmodified = None;
|
let mut tab_to_close_unmodified = None;
|
||||||
let mut tab_to_close_modified = None;
|
let mut tab_to_close_modified = None;
|
||||||
@ -39,11 +34,8 @@ pub(crate) fn tab_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
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)
|
|
||||||
.sense(egui::Sense::click()),
|
|
||||||
);
|
|
||||||
if tab_response.clicked() {
|
if tab_response.clicked() {
|
||||||
tab_to_switch = Some(i);
|
tab_to_switch = Some(i);
|
||||||
}
|
}
|
||||||
@ -53,10 +45,7 @@ pub(crate) fn tab_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
let close_button = egui::Button::new("×")
|
let close_button = egui::Button::new("×")
|
||||||
.small()
|
.small()
|
||||||
.fill(visuals.panel_fill)
|
.fill(visuals.panel_fill)
|
||||||
.stroke(egui::Stroke::new(
|
.stroke(egui::Stroke::new(0.0, egui::Color32::from_rgb(0, 0, 0)));
|
||||||
0.0,
|
|
||||||
egui::Color32::from_rgb(0, 0, 0),
|
|
||||||
));
|
|
||||||
let close_response = ui.add(close_button);
|
let close_response = ui.add(close_button);
|
||||||
if close_response.clicked() {
|
if close_response.clicked() {
|
||||||
if *is_modified {
|
if *is_modified {
|
||||||
@ -94,7 +83,6 @@ pub(crate) fn tab_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
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