better state caching, started building custom syntax theme

This commit is contained in:
candle 2025-07-26 11:50:48 -04:00
parent e5b1214f63
commit 0c7ae2d1b1
14 changed files with 419 additions and 200 deletions

View File

@ -8,7 +8,7 @@ eframe = "0.32"
egui = "0.32" egui = "0.32"
egui_extras = { version = "0.32", features = ["syntect"] } 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" serde_json = "1.0.141"
rfd = "0.15.4" rfd = "0.15.4"
toml = "0.9.2" toml = "0.9.2"
dirs = "6.0" dirs = "6.0"

View File

@ -1,6 +1,8 @@
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 {
@ -19,13 +21,44 @@ impl TextEditor {
} }
} }
pub fn from_config_with_context(config: Config, cc: &eframe::CreationContext<'_>) -> Self { pub fn from_config_with_context(
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() { 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

@ -6,7 +6,7 @@ use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CachedTab { pub struct CachedTab {
pub diff_file: Option<PathBuf>, // Path to diff file for modified tabs pub diff_file: Option<PathBuf>,
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,

View File

@ -53,18 +53,6 @@ impl TextEditor {
self.save_config(); self.save_config();
} }
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;
}
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);
}
}
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();

154
src/io.rs
View File

@ -7,6 +7,116 @@ 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", &["*"])
@ -55,6 +165,50 @@ 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

@ -3,6 +3,7 @@
use eframe::egui; use eframe::egui;
use std::env; use std::env;
use std::io::IsTerminal; use std::io::IsTerminal;
use std::path::PathBuf;
mod app; mod app;
mod io; mod io;
@ -10,10 +11,12 @@ 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 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() { if std::io::stdin().is_terminal() {
println!("This is a GUI application, are you sure you want to launch from terminal?"); println!("This is a GUI application, are you sure you want to launch from terminal?");
// return Ok(());
} }
let options = eframe::NativeOptions { let options = eframe::NativeOptions {
@ -29,6 +32,12 @@ fn main() -> eframe::Result {
eframe::run_native( eframe::run_native(
"ced", "ced",
options, options,
Box::new(move |cc| Ok(Box::new(TextEditor::from_config_with_context(config, cc)))), Box::new(move |cc| {
Ok(Box::new(TextEditor::from_config_with_context(
config,
cc,
initial_paths,
)))
}),
) )
} }

View File

@ -1,5 +1,6 @@
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,4 +1,5 @@
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) {
@ -16,20 +17,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(8), corner_radius: egui::CornerRadius::same(CORNER_RADIUS),
shadow: visuals.window_shadow, shadow: visuals.window_shadow,
inner_margin: egui::Margin::same(16), inner_margin: egui::Margin::same(INNER_MARGIN),
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(14.0) .size(UI_TEXT_SIZE)
.weak(), .weak(),
); );
ui.add_space(12.0); ui.add_space(LARGE);
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

@ -4,6 +4,7 @@ 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;
@ -74,13 +75,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(3.0); ui.add_space(SMALL);
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(4.0); ui.add_space(SMALL);
}; };
egui::ScrollArea::vertical() egui::ScrollArea::vertical()

View File

@ -103,8 +103,8 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
let mut layouter = |ui: &egui::Ui, string: &dyn egui::TextBuffer, wrap_width: f32| { let mut layouter = |ui: &egui::Ui, string: &dyn egui::TextBuffer, wrap_width: f32| {
// let syntect_theme = // let syntect_theme =
// crate::app::theme::create_code_theme_from_visuals(ui.visuals(), font_size); // 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 text = string.as_str();
let theme = egui_extras::syntax_highlighting::CodeTheme::dark(font_size);
let mut layout_job = if syntax_highlighting_enabled && language != "txt" { let mut layout_job = if syntax_highlighting_enabled && language != "txt" {
// let mut settings = egui_extras::syntax_highlighting::SyntectSettings::default(); // let mut settings = egui_extras::syntax_highlighting::SyntectSettings::default();
// settings.ts = syntect_theme; // settings.ts = syntect_theme;

26
src/ui/constants.rs Normal file
View File

@ -0,0 +1,26 @@
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,4 +1,5 @@
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) {
@ -37,9 +38,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(8), corner_radius: egui::CornerRadius::same(CORNER_RADIUS),
shadow: visuals.window_shadow, shadow: visuals.window_shadow,
inner_margin: egui::Margin::same(16), inner_margin: egui::Margin::same(INNER_MARGIN),
outer_margin: egui::Margin::same(0), outer_margin: egui::Margin::same(0),
}) })
.show(ctx, |ui| { .show(ctx, |ui| {
@ -78,7 +79,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(4.0); ui.add_space(SMALL);
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)
@ -88,7 +89,7 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
}); });
} }
ui.add_space(8.0); ui.add_space(MEDIUM);
ui.horizontal(|ui| { ui.horizontal(|ui| {
let case_sensitive_changed = ui let case_sensitive_changed = ui
@ -98,7 +99,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(8.0); ui.add_space(MEDIUM);
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();
@ -117,7 +118,7 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
} }
}); });
ui.add_space(8.0); 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() {
@ -139,7 +140,7 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
should_close = true; should_close = true;
} }
ui.add_space(4.0); ui.add_space(SMALL);
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

@ -1,11 +1,14 @@
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 = (screen_rect.width() * 0.6).clamp(300.0, 400.0); let window_width = (screen_rect.width() * WINDOW_WIDTH_RATIO)
let window_height = (screen_rect.height() * 0.7).clamp(250.0, 500.0); .clamp(WINDOW_MIN_WIDTH, WINDOW_MAX_WIDTH);
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")
@ -19,169 +22,168 @@ 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(8), corner_radius: egui::CornerRadius::same(CORNER_RADIUS),
shadow: visuals.window_shadow, shadow: visuals.window_shadow,
inner_margin: egui::Margin::same(16), inner_margin: egui::Margin::same(INNER_MARGIN),
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("General Settings"); ui.heading("Editor Settings");
ui.add_space(8.0); ui.add_space(MEDIUM);
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui ui.vertical(|ui| {
.checkbox(&mut app.state_cache, "Cache State") if ui
.on_hover_text("Unsaved changes will be cached between sessions") .checkbox(&mut app.state_cache, "Maintain State")
.changed() .on_hover_text("Unsaved changes will be cached between sessions")
{ .changed()
app.save_config(); {
if !app.state_cache { app.save_config();
if let Err(e) = TextEditor::clear_state_cache() { if !app.state_cache {
eprintln!("Failed to clear state cache: {e}"); 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(4.0); ui.add_space(SMALL);
ui.horizontal(|ui| {
if ui
.checkbox(&mut app.show_line_numbers, "Show Line Numbers")
.changed()
{
app.save_config();
}
if ui
.checkbox(&mut app.syntax_highlighting, "Syntax Highlighting")
.changed()
{
app.save_config();
}
});
ui.add_space(4.0);
ui.horizontal(|ui| {
if ui.checkbox(&mut app.word_wrap, "Word Wrap").changed() {
app.save_config();
}
});
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();
}
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.separator();
ui.add_space(LARGE);
ui.heading("Font Settings"); ui.heading("Font Settings");
ui.add_space(8.0); ui.add_space(MEDIUM);
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Font Family:"); ui.vertical(|ui| {
ui.add_space(5.0); ui.label("Font Family:");
ui.add_space(SMALL);
ui.label("Font Size:");
});
let mut changed = false; ui.vertical(|ui| {
egui::ComboBox::from_id_salt("font_family") let mut changed = false;
.selected_text(&app.font_family) egui::ComboBox::from_id_salt("font_family")
.show_ui(ui, |ui| { .selected_text(&app.font_family)
if ui .show_ui(ui, |ui| {
.selectable_value( if ui
&mut app.font_family, .selectable_value(
"Proportional".to_string(), &mut app.font_family,
"Proportional", "Proportional".to_string(),
) "Proportional",
.clicked() )
{ .clicked()
changed = true; {
changed = true;
}
if ui
.selectable_value(
&mut app.font_family,
"Monospace".to_string(),
"Monospace",
)
.clicked()
{
changed = true;
}
});
if app.font_size_input.is_none() {
app.font_size_input = Some(app.font_size.to_string());
}
let mut font_size_text = app
.font_size_input
.as_ref()
.unwrap_or(&DEFAULT_FONT_SIZE_STR.to_string())
.to_owned();
ui.add_space(SMALL);
ui.horizontal(|ui| {
let response = ui.add(
egui::TextEdit::singleline(&mut font_size_text)
.desired_width(FONT_SIZE_INPUT_WIDTH)
.hint_text(DEFAULT_FONT_SIZE_STR)
.id(egui::Id::new("font_size_input")),
);
app.font_size_input = Some(font_size_text.to_owned());
if response.clicked() {
response.request_focus();
} }
if ui
.selectable_value( ui.label("px");
&mut app.font_family,
"Monospace".to_string(), if response.lost_focus() {
"Monospace", if let Ok(new_size) = font_size_text.parse::<f32>() {
) let clamped_size = new_size.clamp(MIN_FONT_SIZE, MAX_FONT_SIZE);
.clicked() if (app.font_size - clamped_size).abs() > 0.1 {
{ app.font_size = clamped_size;
changed = true; app.apply_font_settings(ctx);
}
}
app.font_size_input = None;
} }
});
if changed { 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() {
app.font_size_input = Some(app.font_size.to_string());
}
let mut font_size_text = app
.font_size_input
.as_ref()
.unwrap_or(&"14".to_string())
.to_owned();
let response = ui.add(
egui::TextEdit::singleline(&mut font_size_text)
.desired_width(50.0)
.hint_text("14")
.id(egui::Id::new("font_size_input")),
);
app.font_size_input = Some(font_size_text.to_owned());
if response.clicked() {
response.request_focus();
}
ui.label("px");
if response.lost_focus() {
if let Ok(new_size) = font_size_text.parse::<f32>() {
let clamped_size = new_size.clamp(8.0, 32.0);
if (app.font_size - clamped_size).abs() > 0.1 {
app.font_size = clamped_size;
app.apply_font_settings(ctx); app.apply_font_settings(ctx);
} }
} })
app.font_size_input = None; });
}
}); });
ui.add_space(8.0); ui.add_space(MEDIUM);
ui.label("Preview:"); ui.label("Preview:");
ui.add_space(4.0); ui.add_space(SMALL);
egui::ScrollArea::vertical() egui::ScrollArea::vertical()
.max_height(150.0) .max_height(PREVIEW_AREA_MAX_HEIGHT)
.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(8)) .inner_margin(egui::Margin::same(INNER_MARGIN))
.show(ui, |ui| { .show(ui, |ui| {
let preview_font = egui::FontId::new( let preview_font = egui::FontId::new(
app.font_size, app.font_size,
@ -211,7 +213,7 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
}); });
}); });
ui.add_space(12.0); ui.add_space(LARGE);
if ui.button("Close").clicked() { if ui.button("Close").clicked() {
app.show_preferences = false; app.show_preferences = false;

View File

@ -1,39 +1,42 @@
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(egui::RichText::new("Navigation").size(18.0).strong()); ui.label(egui::RichText::new("Navigation").size(UI_HEADER_SIZE).strong());
ui.label(egui::RichText::new("Ctrl + N: New").size(14.0)); ui.label(egui::RichText::new("Ctrl + N: New").size(UI_TEXT_SIZE));
ui.label(egui::RichText::new("Ctrl + O: Open").size(14.0)); ui.label(egui::RichText::new("Ctrl + O: Open").size(UI_TEXT_SIZE));
ui.label(egui::RichText::new("Ctrl + S: Save").size(14.0)); ui.label(egui::RichText::new("Ctrl + S: Save").size(UI_TEXT_SIZE));
ui.label(egui::RichText::new("Ctrl + Shift + S: Save As").size(14.0)); ui.label(egui::RichText::new("Ctrl + Shift + S: Save As").size(UI_TEXT_SIZE));
ui.label(egui::RichText::new("Ctrl + T: New Tab").size(14.0)); ui.label(egui::RichText::new("Ctrl + T: New Tab").size(UI_TEXT_SIZE));
ui.label(egui::RichText::new("Ctrl + Tab: Next Tab").size(14.0)); ui.label(egui::RichText::new("Ctrl + W: Close Tab").size(UI_TEXT_SIZE));
ui.label(egui::RichText::new("Ctrl + Shift + Tab: Last Tab").size(14.0)); ui.label(egui::RichText::new("Ctrl + Tab: Next Tab").size(UI_TEXT_SIZE));
ui.add_space(16.0); 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(18.0).strong()); ui.label(egui::RichText::new("Editing").size(UI_HEADER_SIZE).strong());
ui.label(egui::RichText::new("Ctrl + Z: Undo").size(14.0)); ui.label(egui::RichText::new("Ctrl + Z: Undo").size(UI_TEXT_SIZE));
ui.label(egui::RichText::new("Ctrl + Shift + Z: Redo").size(14.0)); ui.label(egui::RichText::new("Ctrl + Shift + Z: Redo").size(UI_TEXT_SIZE));
ui.label(egui::RichText::new("Ctrl + X: Cut").size(14.0)); ui.label(egui::RichText::new("Ctrl + X: Cut").size(UI_TEXT_SIZE));
ui.label(egui::RichText::new("Ctrl + C: Copy").size(14.0)); ui.label(egui::RichText::new("Ctrl + C: Copy").size(UI_TEXT_SIZE));
ui.label(egui::RichText::new("Ctrl + V: Paste").size(14.0)); ui.label(egui::RichText::new("Ctrl + V: Paste").size(UI_TEXT_SIZE));
ui.label(egui::RichText::new("Ctrl + A: Select All").size(14.0)); ui.label(egui::RichText::new("Ctrl + A: Select All").size(UI_TEXT_SIZE));
ui.label(egui::RichText::new("Ctrl + D: Delete Line").size(14.0)); ui.label(egui::RichText::new("Ctrl + D: Delete Line").size(UI_TEXT_SIZE));
ui.label(egui::RichText::new("Ctrl + F: Find").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(16.0); ui.add_space(VLARGE);
ui.separator(); ui.separator();
ui.label(egui::RichText::new("Views").size(18.0).strong()); ui.label(egui::RichText::new("Views").size(UI_HEADER_SIZE).strong());
ui.label(egui::RichText::new("Ctrl + L: Toggle Line Numbers").size(14.0)); ui.label(egui::RichText::new("Ctrl + L: Toggle Line Numbers").size(UI_TEXT_SIZE));
ui.label(egui::RichText::new("Ctrl + Shift + L: Change Line Number Side").size(14.0)); ui.label(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 + K: Toggle Word Wrap").size(UI_TEXT_SIZE));
ui.label(egui::RichText::new("Ctrl + H: Toggle Auto Hide Toolbar").size(14.0)); ui.label(egui::RichText::new("Ctrl + H: Toggle Auto Hide Toolbar").size(UI_TEXT_SIZE));
ui.label(egui::RichText::new("Ctrl + P: Preferences").size(14.0)); ui.label(egui::RichText::new("Ctrl + P: Preferences").size(UI_TEXT_SIZE));
ui.label(egui::RichText::new("Ctrl + =/-: Increase/Decrease Font Size").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(14.0)); 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)
@ -42,7 +45,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(16.0); ui.add_space(VLARGE);
ui.separator(); ui.separator();
}); });
} }
@ -51,8 +54,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 = (screen_rect.width() * 0.6).clamp(300.0, 400.0); let window_width = (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)
@ -64,9 +67,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(8), corner_radius: egui::CornerRadius::same(CORNER_RADIUS),
shadow: visuals.window_shadow, shadow: visuals.window_shadow,
inner_margin: egui::Margin::same(16), inner_margin: egui::Margin::same(INNER_MARGIN),
outer_margin: egui::Margin::same(0), outer_margin: egui::Margin::same(0),
}) })
.show(ctx, |ui| { .show(ctx, |ui| {
@ -85,7 +88,7 @@ pub(crate) fn shortcuts_window(app: &mut TextEditor, ctx: &egui::Context) {
); );
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.add_space(8.0); ui.add_space(MEDIUM);
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)