Compare commits

..

No commits in common. "5b1634825e04838346b8c59b8bd2349b6f27adb1" and "51063aac44d2b7e4b6cc98063037b8a5d3b94db7" have entirely different histories.

22 changed files with 326 additions and 854 deletions

View File

@ -1,19 +1,15 @@
[package] [package]
name = "ced" name = "ced"
version = "0.1.3" version = "0.0.9"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
eframe = "0.32" eframe = "0.32"
egui = "0.32" egui = "0.32"
egui_extras = { version = "0.32", features = ["syntect"] }
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.141" serde_json = "1.0"
rfd = "0.15.4" rfd = "0.15.4"
toml = "0.9.2" toml = "0.9.2"
dirs = "6.0" dirs = "6.0"
libc = "0.2.174" libc = "0.2.174"
syntect = "5.2.0"
plist = "1.7.4"
diffy = "0.4.2" diffy = "0.4.2"
uuid = { version = "1.0", features = ["v4"] }

View File

@ -9,7 +9,7 @@ There is a disturbing lack of simple GUI text editors available on Linux nativel
## Features ## Features
* Sane text editing with standard keybindings (`Ctrl+A`, `Ctrl+C`, etc.). * Sane text editing with standard keybindings (`Ctrl+A`, `Ctrl+C`, etc.).
* Choose between opening fresh every time, like Notepad, or maintaining a consistent state like Notepad++. * Opens with a blank slate for quick typing, remember Notepad?
* Separate UI zoom that doesn't affect font size (`Ctrl+Shift` + `+`/`-`). * Separate UI zoom that doesn't affect font size (`Ctrl+Shift` + `+`/`-`).
* Ricers rejoice, your `pywal` colors will be used! * Ricers rejoice, your `pywal` colors will be used!
* Weirdly smooth typing experience. * Weirdly smooth typing experience.
@ -39,7 +39,6 @@ sudo install -Dm644 ced.desktop /usr/share/applications/ced.desktop
Here is an example `config.toml`: Here is an example `config.toml`:
```toml ```toml
state_cache = true
auto_hide_toolbar = false auto_hide_toolbar = false
show_line_numbers = false show_line_numbers = false
word_wrap = false word_wrap = false
@ -47,18 +46,15 @@ theme = "System"
line_side = false line_side = false
font_family = "Monospace" font_family = "Monospace"
font_size = 16.0 font_size = 16.0
syntax_highlighting = true
``` ```
### Options ### Options
| Option | Default | Description | | Option | Default | Description |
|--------|---------|-------------| |--------|---------|-------------|
| `state_cache` | `false` | If `true`, open files will have their unsaved changes cached and will be automatically opened when starting a new session. |
| `auto_hide_toolbar` | `false` | If `true`, the menu bar at the top will be hidden. Move your mouse to the top of the window to reveal it. | | `auto_hide_toolbar` | `false` | If `true`, the menu bar at the top will be hidden. Move your mouse to the top of the window to reveal it. |
| `hide_tab_bar` | 'true' | If `false`, a separate tab bar will be drawn below the toolbar. | | `hide_tab_bar` | 'true' | If `false`, a separate tab bar will be drawn below the toolbar. |
| `show_line_numbers` | `false` | If `true`, line numbers will be displayed on the side specified by `line_side`. | | `show_line_numbers` | `false` | If `true`, line numbers will be displayed on the side specified by `line_side`. |
| `syntax_highlighting` | `false` | If `true`, text will be highlighted based on detected language. |
| `line_side` | `false` | If `false`, line numbers are on the left. If `true`, they are on the right. | | `line_side` | `false` | If `false`, line numbers are on the left. If `true`, they are on the right. |
| `word_wrap` | `false` | If `true`, lines will wrap when they reach the edge of the window. | | `word_wrap` | `false` | If `true`, lines will wrap when they reach the edge of the window. |
| `font_family` | `"Proportional"` | The font family used for the editor text. | | `font_family` | `"Proportional"` | The font family used for the editor text. |
@ -69,7 +65,9 @@ syntax_highlighting = true
In order of importance. In order of importance.
| Feature | Info | | Feature | Info |
| ------- | ---- | | ------- | ---- |
| **LSP:** | Looking at allowing you to use/attach your own tools for this. | | **Find/Replace:** | Functioning. |
| **State/Cache:** | A toggleable option to keep an application state and prevent "Quit without saving" warnings. |
| **Syntax Highlighting/LSP:** | Looking at allowing you to use/attach your own tools for this. |
| **Choose Font** | More than just Monospace/Proportional. | | **Choose Font** | More than just Monospace/Proportional. |
| **Vim Mode:** | It's in-escapable. | | **Vim Mode:** | It's in-escapable. |
| **CLI Mode:** | 💀 | | **CLI Mode:** | 💀 |

View File

@ -1,8 +1,6 @@
use super::editor::TextEditor; use super::editor::TextEditor;
use crate::app::config::Config; use crate::app::config::Config;
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 {
@ -21,44 +19,14 @@ impl TextEditor {
} }
} }
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);
// Load state cache if enabled
if let Err(e) = editor.load_state_cache() { if let Err(e) = editor.load_state_cache() {
eprintln!("Failed to load state cache: {e}"); 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);

View File

@ -30,6 +30,7 @@ impl TextEditor {
let search_slice = if search_content.is_char_boundary(start) { let search_slice = if search_content.is_char_boundary(start) {
&search_content[start..] &search_content[start..]
} else { } else {
// Find next valid boundary
while start < search_content.len() && !search_content.is_char_boundary(start) { while start < search_content.len() && !search_content.is_char_boundary(start) {
start += 1; start += 1;
} }
@ -44,6 +45,7 @@ impl TextEditor {
self.find_matches self.find_matches
.push((absolute_pos, absolute_pos + query.len())); .push((absolute_pos, absolute_pos + query.len()));
// Advance to next valid character boundary instead of just +1
start = absolute_pos + 1; start = absolute_pos + 1;
while start < search_content.len() && !search_content.is_char_boundary(start) { while start < search_content.len() && !search_content.is_char_boundary(start) {
start += 1; start += 1;

View File

@ -1,12 +1,11 @@
use super::editor::TextEditor; use super::editor::TextEditor;
use crate::app::tab::{compute_content_hash, Tab}; use crate::app::tab::{Tab, compute_content_hash};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::PathBuf; use std::path::PathBuf;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CachedTab { pub struct CachedTab {
pub diff_file: Option<PathBuf>, pub diff: Option<String>,
pub full_content: Option<String>, // This is used for 'new files' that don't have a path pub full_content: Option<String>, // This is used for 'new files' that don't have a path
pub file_path: Option<PathBuf>, pub file_path: Option<PathBuf>,
pub is_modified: bool, pub is_modified: bool,
@ -21,40 +20,18 @@ pub struct StateCache {
pub tab_counter: 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 { impl From<&Tab> for CachedTab {
fn from(tab: &Tab) -> Self { fn from(tab: &Tab) -> Self {
if let Some(file_path) = &tab.file_path { if let Some(file_path) = &tab.file_path {
let original_content = std::fs::read_to_string(file_path).unwrap_or_default(); let original_content = std::fs::read_to_string(file_path).unwrap_or_default();
let diff_file = if tab.is_modified { let diff = if tab.is_modified {
let diff_content = diffy::create_patch(&original_content, &tab.content); Some(diffy::create_patch(&original_content, &tab.content).to_string())
match create_diff_file(&diff_content.to_string()) {
Ok(path) => Some(path),
Err(e) => {
eprintln!("Warning: Failed to create diff file: {}", e);
None
}
}
} else { } else {
None None
}; };
Self { Self {
diff_file, diff,
full_content: None, full_content: None,
file_path: tab.file_path.clone(), file_path: tab.file_path.clone(),
is_modified: tab.is_modified, is_modified: tab.is_modified,
@ -63,7 +40,7 @@ impl From<&Tab> for CachedTab {
} }
} else { } else {
Self { Self {
diff_file: None, diff: None,
full_content: Some(tab.content.clone()), full_content: Some(tab.content.clone()),
file_path: None, file_path: None,
is_modified: tab.is_modified, is_modified: tab.is_modified,
@ -78,32 +55,21 @@ impl From<CachedTab> for Tab {
fn from(cached: CachedTab) -> Self { fn from(cached: CachedTab) -> Self {
if let Some(file_path) = cached.file_path { if let Some(file_path) = cached.file_path {
let original_content = std::fs::read_to_string(&file_path).unwrap_or_default(); let original_content = std::fs::read_to_string(&file_path).unwrap_or_default();
let current_content = if let Some(diff_path) = cached.diff_file { let current_content = if let Some(diff_str) = cached.diff {
match load_diff_file(&diff_path) { match diffy::Patch::from_str(&diff_str) {
Ok(diff_content) => { Ok(patch) => {
match diffy::Patch::from_str(&diff_content) { match diffy::apply(&original_content, &patch) {
Ok(patch) => match diffy::apply(&original_content, &patch) {
Ok(content) => content, Ok(content) => content,
Err(_) => { Err(_) => {
eprintln!("Warning: Failed to apply diff for {}, using original content", eprintln!("Warning: Failed to apply diff for {}, using original content",
file_path.display()); file_path.display());
original_content original_content
} }
}, }
}
Err(_) => { Err(_) => {
eprintln!( eprintln!("Warning: Failed to parse diff for {}, using original content",
"Warning: Failed to parse diff for {}, using original content", file_path.display());
file_path.display()
);
original_content
}
}
}
Err(e) => {
eprintln!(
"Warning: Failed to load diff file {:?}: {}, using original content",
diff_path, e
);
original_content original_content
} }
} }
@ -111,8 +77,7 @@ impl From<CachedTab> for Tab {
original_content original_content
}; };
let original_hash = let original_hash = compute_content_hash(&std::fs::read_to_string(&file_path).unwrap_or_default());
compute_content_hash(&std::fs::read_to_string(&file_path).unwrap_or_default());
let expected_hash = cached.original_content_hash; let expected_hash = cached.original_content_hash;
let mut tab = Tab::new_with_file(current_content, file_path); let mut tab = Tab::new_with_file(current_content, file_path);
@ -151,37 +116,6 @@ impl TextEditor {
Some(cache_dir.join("state.json")) 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>> { pub fn load_state_cache(&mut self) -> Result<(), Box<dyn std::error::Error>> {
if !self.state_cache { if !self.state_cache {
return Ok(()); return Ok(());
@ -198,8 +132,7 @@ impl TextEditor {
if !state_cache.tabs.is_empty() { if !state_cache.tabs.is_empty() {
self.tabs = state_cache.tabs.into_iter().map(Tab::from).collect(); self.tabs = state_cache.tabs.into_iter().map(Tab::from).collect();
self.active_tab_index = self.active_tab_index = std::cmp::min(state_cache.active_tab_index, self.tabs.len() - 1);
std::cmp::min(state_cache.active_tab_index, self.tabs.len() - 1);
self.tab_counter = state_cache.tab_counter; self.tab_counter = state_cache.tab_counter;
self.text_needs_processing = true; self.text_needs_processing = true;
} }
@ -224,13 +157,6 @@ impl TextEditor {
tab_counter: self.tab_counter, 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)?; let content = serde_json::to_string_pretty(&state_cache)?;
std::fs::write(&cache_path, content)?; std::fs::write(&cache_path, content)?;
@ -243,13 +169,6 @@ impl TextEditor {
std::fs::remove_file(cache_path)?; 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(()) Ok(())
} }
} }

View File

@ -19,6 +19,7 @@ impl TextEditor {
} }
self.text_needs_processing = true; self.text_needs_processing = true;
// Save state cache after adding new tab
if let Err(e) = self.save_state_cache() { if let Err(e) = self.save_state_cache() {
eprintln!("Failed to save state cache: {e}"); eprintln!("Failed to save state cache: {e}");
} }
@ -37,6 +38,7 @@ impl TextEditor {
} }
self.text_needs_processing = true; self.text_needs_processing = true;
// Save state cache after closing tab
if let Err(e) = self.save_state_cache() { if let Err(e) = self.save_state_cache() {
eprintln!("Failed to save state cache: {e}"); eprintln!("Failed to save state cache: {e}");
} }
@ -51,6 +53,7 @@ impl TextEditor {
} }
self.text_needs_processing = true; self.text_needs_processing = true;
// Save state cache after switching tabs
if let Err(e) = self.save_state_cache() { if let Err(e) = self.save_state_cache() {
eprintln!("Failed to save state cache: {e}"); eprintln!("Failed to save state cache: {e}");
} }

View File

@ -23,6 +23,7 @@ impl TextEditor {
} }
} }
/// Get the configured font ID based on the editor's font settings
pub fn get_font_id(&self) -> egui::FontId { pub fn get_font_id(&self) -> egui::FontId {
let font_family = match self.font_family.as_str() { let font_family = match self.font_family.as_str() {
"Monospace" => egui::FontFamily::Monospace, "Monospace" => egui::FontFamily::Monospace,
@ -31,11 +32,13 @@ impl TextEditor {
egui::FontId::new(self.font_size, font_family) egui::FontId::new(self.font_size, font_family)
} }
/// Immediately apply theme and save to configuration
pub fn set_theme(&mut self, ctx: &egui::Context) { pub fn set_theme(&mut self, ctx: &egui::Context) {
theme::apply(self.theme, ctx); theme::apply(self.theme, ctx);
self.save_config(); self.save_config();
} }
/// Apply font settings with immediate text reprocessing
pub fn apply_font_settings(&mut self, ctx: &egui::Context) { pub fn apply_font_settings(&mut self, ctx: &egui::Context) {
let font_family = match self.font_family.as_str() { let font_family = match self.font_family.as_str() {
"Monospace" => egui::FontFamily::Monospace, "Monospace" => egui::FontFamily::Monospace,
@ -53,6 +56,21 @@ impl TextEditor {
self.save_config(); self.save_config();
} }
/// Apply font settings with immediate text reprocessing
pub fn apply_font_settings_with_ui(&mut self, ctx: &egui::Context, ui: &egui::Ui) {
self.apply_font_settings(ctx);
self.reprocess_text_for_font_change(ui);
self.font_settings_changed = false;
}
/// Trigger immediate text reprocessing when font settings change
pub fn reprocess_text_for_font_change(&mut self, ui: &egui::Ui) {
if let Some(active_tab) = self.get_active_tab() {
self.process_text_for_rendering(&active_tab.content.to_string(), ui);
}
}
/// Calculates the available width for the text editor, accounting for line numbers and separator
pub fn calculate_editor_dimensions(&self, ui: &egui::Ui) -> EditorDimensions { pub fn calculate_editor_dimensions(&self, ui: &egui::Ui) -> EditorDimensions {
let total_available_width = ui.available_width(); let total_available_width = ui.available_width();
@ -96,6 +114,7 @@ impl TextEditor {
} }
} }
/// Calculate the available width for non-word-wrapped content based on processed text data
pub fn calculate_content_based_width(&self, ui: &egui::Ui) -> f32 { pub fn calculate_content_based_width(&self, ui: &egui::Ui) -> f32 {
let processing_result = self.get_text_processing_result(); let processing_result = self.get_text_processing_result();

View File

@ -34,7 +34,7 @@ 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 hash = compute_content_hash(&content);

View File

@ -1,9 +1,4 @@
use eframe::egui; use eframe::egui;
use plist::{Dictionary, Value};
use std::collections::BTreeMap;
use syntect::highlighting::{
Color as SyntectColor, Theme as SyntectTheme, ThemeSet, ThemeSettings, UnderlineOption,
};
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize, Default)] #[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize, Default)]
pub enum Theme { pub enum Theme {
@ -199,127 +194,3 @@ fn detect_system_dark_mode() -> bool {
true true
} }
} }
fn egui_color_to_syntect(color: egui::Color32) -> SyntectColor {
SyntectColor {
r: color.r(),
g: color.g(),
b: color.b(),
a: color.a(),
}
}
pub fn create_code_theme_from_visuals(visuals: &egui::Visuals, font_size: f32) -> ThemeSet {
let text_color = visuals.override_text_color.unwrap_or(visuals.text_color());
let bg_color = visuals.extreme_bg_color;
let selection_color = visuals.selection.bg_fill;
let comment_color = blend_colors(text_color, bg_color, 0.6);
let keyword_color = if visuals.dark_mode {
blend_colors(egui::Color32::from_rgb(100, 149, 237), text_color, 0.8) // CornflowerBlue-like
} else {
blend_colors(egui::Color32::from_rgb(0, 0, 139), text_color, 0.8) // DarkBlue-like
};
let string_color = if visuals.dark_mode {
blend_colors(egui::Color32::from_rgb(144, 238, 144), text_color, 0.8) // LightGreen-like
} else {
blend_colors(egui::Color32::from_rgb(0, 128, 0), text_color, 0.8) // Green-like
};
let number_color = if visuals.dark_mode {
blend_colors(egui::Color32::from_rgb(255, 165, 0), text_color, 0.8) // Orange-like
} else {
blend_colors(egui::Color32::from_rgb(165, 42, 42), text_color, 0.8) // Brown-like
};
let function_color = if visuals.dark_mode {
blend_colors(egui::Color32::from_rgb(255, 20, 147), text_color, 0.8) // DeepPink-like
} else {
blend_colors(egui::Color32::from_rgb(128, 0, 128), text_color, 0.8) // Purple-like
};
let plist_theme = build_custom_theme_plist(
"System",
&format!("{:?}", bg_color),
&format!("{:?}", text_color),
&format!("{:?}", comment_color),
&format!("{:?}", string_color),
&format!("{:?}", keyword_color),
);
let file = std::fs::File::create("system.tmTheme").unwrap();
let writer = std::io::BufWriter::new(file);
let _ = plist::to_writer_xml(writer, &plist_theme);
let loaded_file = std::fs::File::open("system.tmTheme").unwrap();
let mut loaded_reader = std::io::BufReader::new(loaded_file);
let loaded_theme = ThemeSet::load_from_reader(&mut loaded_reader).unwrap();
let mut set = ThemeSet::new();
set.add_from_folder(".").unwrap();
return set;
}
fn build_custom_theme_plist(
theme_name: &str,
background_color: &str,
foreground_color: &str,
comment_color: &str,
string_color: &str,
keyword_color: &str,
) -> Value {
let mut root_dict = Dictionary::new();
root_dict.insert("name".to_string(), Value::String(theme_name.to_string()));
let mut settings_array = Vec::new();
let mut global_settings_dict = Dictionary::new();
let mut inner_global_settings = Dictionary::new();
inner_global_settings.insert(
"background".to_string(),
Value::String(background_color.to_string()),
);
inner_global_settings.insert(
"foreground".to_string(),
Value::String(foreground_color.to_string()),
);
global_settings_dict.insert(
"settings".to_string(),
Value::Dictionary(inner_global_settings),
);
settings_array.push(Value::Dictionary(global_settings_dict));
let mut comment_scope_dict = Dictionary::new();
comment_scope_dict.insert("name".to_string(), Value::String("Comment".to_string()));
comment_scope_dict.insert("scope".to_string(), Value::String("comment".to_string()));
let mut comment_settings = Dictionary::new();
comment_settings.insert(
"foreground".to_string(),
Value::String(comment_color.to_string()),
);
comment_settings.insert("fontStyle".to_string(), Value::String("italic".to_string()));
comment_scope_dict.insert("settings".to_string(), Value::Dictionary(comment_settings));
settings_array.push(Value::Dictionary(comment_scope_dict));
let mut string_scope_dict = Dictionary::new();
string_scope_dict.insert("name".to_string(), Value::String("String".to_string()));
string_scope_dict.insert("scope".to_string(), Value::String("string".to_string()));
let mut string_settings = Dictionary::new();
string_settings.insert(
"foreground".to_string(),
Value::String(string_color.to_string()),
);
string_scope_dict.insert("settings".to_string(), Value::Dictionary(string_settings));
settings_array.push(Value::Dictionary(string_scope_dict));
let mut keyword_scope_dict = Dictionary::new();
keyword_scope_dict.insert("name".to_string(), Value::String("Keyword".to_string()));
keyword_scope_dict.insert("scope".to_string(), Value::String("keyword".to_string()));
let mut keyword_settings = Dictionary::new();
keyword_settings.insert(
"foreground".to_string(),
Value::String(keyword_color.to_string()),
);
keyword_scope_dict.insert("settings".to_string(), Value::Dictionary(keyword_settings));
settings_array.push(Value::Dictionary(keyword_scope_dict));
root_dict.insert("settings".to_string(), Value::Array(settings_array));
Value::Dictionary(root_dict)
}

154
src/io.rs
View File

@ -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", &["*"])
@ -165,50 +55,6 @@ pub(crate) fn open_file(app: &mut TextEditor) {
} }
} }
pub(crate) fn open_file_from_path(app: &mut TextEditor, path: PathBuf) -> Result<(), String> {
match fs::read_to_string(&path) {
Ok(content) => {
let should_replace_current_tab = if let Some(active_tab) = app.get_active_tab() {
active_tab.file_path.is_none()
&& active_tab.content.is_empty()
&& !active_tab.is_modified
} else {
false
};
if should_replace_current_tab {
if let Some(active_tab) = app.get_active_tab_mut() {
let title = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("Untitled");
active_tab.content = content;
active_tab.file_path = Some(path.to_path_buf());
active_tab.title = title.to_string();
active_tab.mark_as_saved();
}
app.text_needs_processing = true;
} else {
let new_tab = Tab::new_with_file(content, path);
app.tabs.push(new_tab);
app.active_tab_index = app.tabs.len() - 1;
app.text_needs_processing = true;
}
if app.show_find && !app.find_query.is_empty() {
app.update_find_matches();
}
if let Err(e) = app.save_state_cache() {
eprintln!("Failed to save state cache: {e}");
}
Ok(())
}
Err(err) => Err(format!("Failed to open file {}: {}", path.display(), err)),
}
}
pub(crate) fn save_file(app: &mut TextEditor) { pub(crate) fn save_file(app: &mut TextEditor) {
if let Some(active_tab) = app.get_active_tab() { if let Some(active_tab) = app.get_active_tab() {
if let Some(path) = &active_tab.file_path { if let Some(path) = &active_tab.file_path {

View File

@ -1,9 +1,6 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use eframe::egui; use eframe::egui;
use std::env;
use std::io::IsTerminal;
use std::path::PathBuf;
mod app; mod app;
mod io; mod io;
@ -11,14 +8,6 @@ mod ui;
use app::{config::Config, TextEditor}; use app::{config::Config, TextEditor};
fn main() -> eframe::Result { fn main() -> eframe::Result {
let args: Vec<String> = env::args().collect();
let initial_paths: Vec<PathBuf> = args.iter().skip(1).map(|arg| PathBuf::from(arg)).collect();
if std::io::stdin().is_terminal() {
println!("This is a GUI application, are you sure you want to launch from terminal?");
}
let options = eframe::NativeOptions { let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default() viewport: egui::ViewportBuilder::default()
.with_min_inner_size([600.0, 400.0]) .with_min_inner_size([600.0, 400.0])
@ -32,12 +21,6 @@ fn main() -> eframe::Result {
eframe::run_native( eframe::run_native(
"ced", "ced",
options, options,
Box::new(move |cc| { Box::new(move |cc| Ok(Box::new(TextEditor::from_config_with_context(config, cc)))),
Ok(Box::new(TextEditor::from_config_with_context(
config,
cc,
initial_paths,
)))
}),
) )
} }

View File

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

View File

@ -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) {
@ -17,20 +16,20 @@ pub(crate) fn about_window(app: &mut TextEditor, ctx: &egui::Context) {
.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)

View File

@ -1,10 +1,8 @@
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 egui::UiKind;
@ -75,13 +73,13 @@ 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;
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()

View File

@ -1,6 +1,5 @@
use crate::app::TextEditor; use crate::app::TextEditor;
use eframe::egui; use eframe::egui;
use egui_extras::syntax_highlighting::{self};
use super::find_highlight; use super::find_highlight;
@ -13,8 +12,6 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
let show_shortcuts = app.show_shortcuts; let show_shortcuts = app.show_shortcuts;
let word_wrap = app.word_wrap; let word_wrap = app.word_wrap;
let font_size = app.font_size; let font_size = app.font_size;
let font_id = app.get_font_id();
let syntax_highlighting_enabled = app.syntax_highlighting;
let reset_zoom_key = egui::Id::new("editor_reset_zoom"); let reset_zoom_key = egui::Id::new("editor_reset_zoom");
let should_reset_zoom = ui let should_reset_zoom = ui
@ -99,39 +96,14 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
f32::INFINITY f32::INFINITY
}; };
let language = super::languages::get_language_from_extension(active_tab.file_path.as_deref());
let mut layouter = |ui: &egui::Ui, string: &dyn egui::TextBuffer, wrap_width: f32| {
// let syntect_theme =
// crate::app::theme::create_code_theme_from_visuals(ui.visuals(), font_size);
let theme = egui_extras::syntax_highlighting::CodeTheme::from_style(ui.style());
let text = string.as_str();
let mut layout_job = if syntax_highlighting_enabled && language != "txt" {
// let mut settings = egui_extras::syntax_highlighting::SyntectSettings::default();
// settings.ts = syntect_theme;
// syntax_highlighting::highlight_with(ui.ctx(), &ui.style().clone(), &theme, text, &language, &settings)
syntax_highlighting::highlight(ui.ctx(), &ui.style().clone(), &theme, text, &language)
} else {
syntax_highlighting::highlight(ui.ctx(), &ui.style().clone(), &theme, text, "")
};
if syntax_highlighting_enabled && language != "txt" {
for section in &mut layout_job.sections {
section.format.font_id = font_id.clone();
}
}
layout_job.wrap.max_width = wrap_width;
ui.fonts(|f| f.layout_job(layout_job))
};
let text_edit = egui::TextEdit::multiline(&mut active_tab.content) let text_edit = egui::TextEdit::multiline(&mut active_tab.content)
.frame(false) .frame(false)
.font(egui::TextStyle::Monospace)
.code_editor() .code_editor()
.desired_width(desired_width) .desired_width(desired_width)
.desired_rows(0) .desired_rows(0)
.lock_focus(!show_find) .lock_focus(!show_find)
.cursor_at_end(false) .cursor_at_end(false)
.layouter(&mut layouter)
.id(egui::Id::new("main_text_editor")); .id(egui::Id::new("main_text_editor"));
let output = if word_wrap { let output = if word_wrap {
@ -158,6 +130,7 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
None None
}; };
// Save state cache when content changes (after releasing the borrow)
if content_changed { if content_changed {
if let Err(e) = app.save_state_cache() { if let Err(e) = app.save_state_cache() {
eprintln!("Failed to save state cache: {e}"); eprintln!("Failed to save state cache: {e}");
@ -196,6 +169,7 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
app.previous_content = content.to_owned(); app.previous_content = content.to_owned();
app.previous_cursor_char_index = current_cursor_pos; app.previous_cursor_char_index = current_cursor_pos;
} }
if app.font_settings_changed || app.text_needs_processing { if app.font_settings_changed || app.text_needs_processing {

View File

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

View File

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

View File

@ -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) {
@ -38,9 +37,9 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
.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| {
@ -79,7 +78,7 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
if app.show_replace_section { if app.show_replace_section {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add_space(SMALL); ui.add_space(4.0);
ui.label("Replace:"); ui.label("Replace:");
let _replace_response = ui.add( let _replace_response = ui.add(
egui::TextEdit::singleline(&mut app.replace_query) egui::TextEdit::singleline(&mut app.replace_query)
@ -89,7 +88,7 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
}); });
} }
ui.add_space(MEDIUM); ui.add_space(8.0);
ui.horizontal(|ui| { ui.horizontal(|ui| {
let case_sensitive_changed = ui let case_sensitive_changed = ui
@ -99,7 +98,7 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
query_changed = true; query_changed = true;
} }
if app.show_replace_section { if app.show_replace_section {
ui.add_space(MEDIUM); ui.add_space(8.0);
let replace_current_enabled = let replace_current_enabled =
!app.find_matches.is_empty() && app.current_match_index.is_some(); !app.find_matches.is_empty() && app.current_match_index.is_some();
@ -118,7 +117,7 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
} }
}); });
ui.add_space(MEDIUM); ui.add_space(8.0);
ui.horizontal(|ui| { ui.horizontal(|ui| {
let match_text = if app.find_matches.is_empty() { let match_text = if app.find_matches.is_empty() {
@ -140,7 +139,7 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
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| {

View File

@ -185,13 +185,6 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
app.save_config(); app.save_config();
ui.close_kind(UiKind::Menu); ui.close_kind(UiKind::Menu);
} }
if ui
.checkbox(&mut app.syntax_highlighting, "Syntax Highlighting")
.clicked()
{
app.save_config();
ui.close_kind(UiKind::Menu);
}
if ui.checkbox(&mut app.word_wrap, "Word Wrap").clicked() { if ui.checkbox(&mut app.word_wrap, "Word Wrap").clicked() {
app.save_config(); app.save_config();
ui.close_kind(UiKind::Menu); ui.close_kind(UiKind::Menu);
@ -283,15 +276,21 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
if app.hide_tab_bar { if app.hide_tab_bar {
let tab_title = if let Some(tab) = app.get_active_tab() { let tab_title = if let Some(tab) = app.get_active_tab() {
tab.get_display_title() tab.title.to_owned()
} else { } else {
let empty_tab = crate::app::tab::Tab::new_empty(1); let empty_tab = crate::app::tab::Tab::new_empty(1);
empty_tab.get_display_title() empty_tab.title.to_owned()
}; };
let window_width = ctx.screen_rect().width(); let window_width = ctx.screen_rect().width();
let font_id = ui.style().text_styles[&egui::TextStyle::Body].to_owned(); let font_id = ui.style().text_styles[&egui::TextStyle::Body].to_owned();
let tab_title = if app.get_active_tab().is_some_and(|tab| tab.is_modified) {
format!("{tab_title}*")
} else {
tab_title
};
let text_galley = ui.fonts(|fonts| { let text_galley = ui.fonts(|fonts| {
fonts.layout_job(egui::text::LayoutJob::simple_singleline( fonts.layout_job(egui::text::LayoutJob::simple_singleline(
tab_title, tab_title,

View File

@ -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).clamp(300.0, 400.0);
(screen_rect.width() * WINDOW_WIDTH_RATIO).clamp(WINDOW_MIN_WIDTH, WINDOW_MAX_WIDTH); let window_height = (screen_rect.height() * 0.7).clamp(250.0, 500.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")
@ -22,21 +19,23 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
.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.heading("General Settings");
ui.add_space(8.0);
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.vertical(|ui| {
if ui if ui
.checkbox(&mut app.state_cache, "Maintain State") .checkbox(&mut app.state_cache, "State Cache")
.on_hover_text("Unsaved changes will be cached between sessions") .on_hover_text(
"Save and restore open tabs and unsaved changes between sessions"
)
.changed() .changed()
{ {
app.save_config(); app.save_config();
@ -46,62 +45,63 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
} }
} }
} }
ui.add_space(SMALL); });
ui.add_space(4.0);
ui.horizontal(|ui| {
if ui if ui
.checkbox(&mut app.show_line_numbers, "Show Line Numbers") .checkbox(&mut app.show_line_numbers, "Show Line Numbers")
.changed() .changed()
{ {
app.save_config(); 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.add_space(4.0);
ui.separator();
ui.add_space(LARGE);
ui.heading("Font Settings");
ui.add_space(MEDIUM);
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.vertical(|ui| { if ui
ui.label("Font Family:"); .checkbox(&mut app.word_wrap, "Word Wrap")
ui.add_space(SMALL); .changed()
ui.label("Font Size:"); {
app.save_config();
}
}); });
ui.vertical(|ui| { ui.add_space(4.0);
ui.horizontal(|ui| {
if ui
.checkbox(&mut app.auto_hide_toolbar, "Auto Hide Toolbar")
.on_hover_text("Hide the menu bar until you move your mouse to the top")
.changed()
{
app.save_config();
}
});
ui.add_space(4.0);
ui.horizontal(|ui| {
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(12.0);
ui.separator();
ui.heading("Font Settings");
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.label("Font Family:");
ui.add_space(5.0);
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,6 +128,17 @@ 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());
} }
@ -135,14 +146,12 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
let mut font_size_text = app let mut font_size_text = app
.font_size_input .font_size_input
.as_ref() .as_ref()
.unwrap_or(&DEFAULT_FONT_SIZE_STR.to_string()) .unwrap_or(&"14".to_string())
.to_owned(); .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")),
); );
@ -156,7 +165,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 +173,20 @@ 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(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,
@ -213,7 +216,7 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
}); });
}); });
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;

View File

@ -1,48 +1,39 @@
use crate::app::TextEditor; use crate::app::TextEditor;
use crate::ui::constants::*;
use eframe::egui; use eframe::egui;
fn render_shortcuts_content(ui: &mut egui::Ui) { fn render_shortcuts_content(ui: &mut egui::Ui) {
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.label( ui.label(egui::RichText::new("Navigation").size(18.0).strong());
egui::RichText::new("Navigation") ui.label(egui::RichText::new("Ctrl + N: New").size(14.0));
.size(UI_HEADER_SIZE) ui.label(egui::RichText::new("Ctrl + O: Open").size(14.0));
.strong(), ui.label(egui::RichText::new("Ctrl + S: Save").size(14.0));
); ui.label(egui::RichText::new("Ctrl + Shift + S: Save As").size(14.0));
ui.label(egui::RichText::new("Ctrl + N: New").size(UI_TEXT_SIZE)); ui.label(egui::RichText::new("Ctrl + T: New Tab").size(14.0));
ui.label(egui::RichText::new("Ctrl + O: Open").size(UI_TEXT_SIZE)); ui.label(egui::RichText::new("Ctrl + Tab: Next Tab").size(14.0));
ui.label(egui::RichText::new("Ctrl + S: Save").size(UI_TEXT_SIZE)); ui.label(egui::RichText::new("Ctrl + Shift + Tab: Last Tab").size(14.0));
ui.label(egui::RichText::new("Ctrl + Shift + S: Save As").size(UI_TEXT_SIZE)); ui.add_space(16.0);
ui.label(egui::RichText::new("Ctrl + T: New Tab").size(UI_TEXT_SIZE));
ui.label(egui::RichText::new("Ctrl + W: Close Tab").size(UI_TEXT_SIZE));
ui.label(egui::RichText::new("Ctrl + Tab: Next Tab").size(UI_TEXT_SIZE));
ui.label(egui::RichText::new("Ctrl + Shift + Tab: Last Tab").size(UI_TEXT_SIZE));
ui.add_space(VLARGE);
ui.separator(); ui.separator();
ui.label(egui::RichText::new("Editing").size(UI_HEADER_SIZE).strong()); ui.label(egui::RichText::new("Editing").size(18.0).strong());
ui.label(egui::RichText::new("Ctrl + Z: Undo").size(UI_TEXT_SIZE)); ui.label(egui::RichText::new("Ctrl + Z: Undo").size(14.0));
ui.label(egui::RichText::new("Ctrl + Shift + Z: Redo").size(UI_TEXT_SIZE)); ui.label(egui::RichText::new("Ctrl + Shift + Z: Redo").size(14.0));
ui.label(egui::RichText::new("Ctrl + X: Cut").size(UI_TEXT_SIZE)); ui.label(egui::RichText::new("Ctrl + X: Cut").size(14.0));
ui.label(egui::RichText::new("Ctrl + C: Copy").size(UI_TEXT_SIZE)); ui.label(egui::RichText::new("Ctrl + C: Copy").size(14.0));
ui.label(egui::RichText::new("Ctrl + V: Paste").size(UI_TEXT_SIZE)); ui.label(egui::RichText::new("Ctrl + V: Paste").size(14.0));
ui.label(egui::RichText::new("Ctrl + A: Select All").size(UI_TEXT_SIZE)); ui.label(egui::RichText::new("Ctrl + A: Select All").size(14.0));
ui.label(egui::RichText::new("Ctrl + D: Delete Line").size(UI_TEXT_SIZE)); ui.label(egui::RichText::new("Ctrl + D: Delete Line").size(14.0));
ui.label(egui::RichText::new("Ctrl + F: Find").size(UI_TEXT_SIZE)); ui.label(egui::RichText::new("Ctrl + F: Find").size(14.0));
ui.label(egui::RichText::new("Ctrl + R: Replace").size(UI_TEXT_SIZE));
ui.add_space(VLARGE); ui.add_space(16.0);
ui.separator(); ui.separator();
ui.label(egui::RichText::new("Views").size(UI_HEADER_SIZE).strong()); ui.label(egui::RichText::new("Views").size(18.0).strong());
ui.label(egui::RichText::new("Ctrl + L: Toggle Line Numbers").size(UI_TEXT_SIZE)); ui.label(egui::RichText::new("Ctrl + L: Toggle Line Numbers").size(14.0));
ui.label( ui.label(egui::RichText::new("Ctrl + Shift + L: Change Line Number Side").size(14.0));
egui::RichText::new("Ctrl + Shift + L: Change Line Number Side").size(UI_TEXT_SIZE), ui.label(egui::RichText::new("Ctrl + K: Toggle Word Wrap").size(14.0));
); ui.label(egui::RichText::new("Ctrl + H: Toggle Auto Hide Toolbar").size(14.0));
ui.label(egui::RichText::new("Ctrl + K: Toggle Word Wrap").size(UI_TEXT_SIZE)); ui.label(egui::RichText::new("Ctrl + P: Preferences").size(14.0));
ui.label(egui::RichText::new("Ctrl + H: Toggle Auto Hide Toolbar").size(UI_TEXT_SIZE)); ui.label(egui::RichText::new("Ctrl + =/-: Increase/Decrease Font Size").size(14.0));
ui.label(egui::RichText::new("Ctrl + P: Preferences").size(UI_TEXT_SIZE)); ui.label(egui::RichText::new("Ctrl + Shift + =/-: Zoom In/Out").size(14.0));
ui.label(egui::RichText::new("Ctrl + =/-: Increase/Decrease Font Size").size(UI_TEXT_SIZE));
ui.label(egui::RichText::new("Ctrl + Shift + =/-: Zoom In/Out").size(UI_TEXT_SIZE));
// ui.label( // ui.label(
// egui::RichText::new("Ctrl + Shift + .: Toggle Vim Mode") // egui::RichText::new("Ctrl + Shift + .: Toggle Vim Mode")
// .size(14.0) // .size(14.0)
@ -51,7 +42,7 @@ fn render_shortcuts_content(ui: &mut egui::Ui) {
// egui::RichText::new("Ctrl + .: Toggle Vim Mode") // egui::RichText::new("Ctrl + .: Toggle Vim Mode")
// .size(14.0) // .size(14.0)
// ); // );
ui.add_space(VLARGE); ui.add_space(16.0);
ui.separator(); ui.separator();
}); });
} }
@ -60,10 +51,8 @@ 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 = let window_width = (screen_rect.width() * 0.6).clamp(300.0, 400.0);
(screen_rect.width() * WINDOW_WIDTH_RATIO).clamp(WINDOW_MIN_WIDTH, WINDOW_MAX_WIDTH); let window_height = (screen_rect.height() * 0.7).clamp(250.0, 500.0);
let window_height =
(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)
@ -75,9 +64,9 @@ pub(crate) fn shortcuts_window(app: &mut TextEditor, ctx: &egui::Context) {
.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| {
@ -96,7 +85,7 @@ pub(crate) fn shortcuts_window(app: &mut TextEditor, ctx: &egui::Context) {
); );
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.add_space(MEDIUM); ui.add_space(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)

View File

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