Merge pull request 'Update to 0.0.4' (#1) from master into release
Reviewed-on: #1
This commit is contained in:
commit
ae6b20d0d4
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
.c*
|
||||
Cargo.lock
|
||||
/target
|
||||
perf.*
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ced"
|
||||
version = "0.0.3"
|
||||
version = "0.0.4"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
25
LICENSE-MIT
Normal file
25
LICENSE-MIT
Normal file
@ -0,0 +1,25 @@
|
||||
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.
|
||||
75
README.md
Normal file
75
README.md
Normal file
@ -0,0 +1,75 @@
|
||||
# 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.).
|
||||
* Opens with a blank slate for quick typing, remember Notepad?
|
||||
* 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
|
||||
auto_hide_toolbar = false
|
||||
show_line_numbers = false
|
||||
word_wrap = false
|
||||
theme = "System"
|
||||
line_side = false
|
||||
font_family = "Monospace"
|
||||
font_size = 16.0
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `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. |
|
||||
| `show_line_numbers` | `false` | If `true`, line numbers will be displayed on the side specified by `line_side`. |
|
||||
| `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 |
|
||||
| ------- | ---- |
|
||||
| **Find/Replace:** | In progress. |
|
||||
| **State/Cache:** | A toggleable option to keep an application state and prevent "Quit without saving" warnings. |
|
||||
| **Syntax Highlighting/LSP:** | Looking at allowing you to use/attach your own tools for this. |
|
||||
| **Choose Font** | More than just Monospace/Proportional. |
|
||||
| **Vim Mode:** | It's in-escapable. |
|
||||
| **CLI Mode:** | 💀 |
|
||||
| **IDE MODE:** | 🤡 |
|
||||
|
||||
I use [Helix](https://helix-editor.com/), btw.
|
||||
@ -6,6 +6,7 @@ use super::theme::Theme;
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub auto_hide_toolbar: bool,
|
||||
pub auto_hide_tab_bar: bool,
|
||||
pub show_line_numbers: bool,
|
||||
pub word_wrap: bool,
|
||||
pub theme: Theme,
|
||||
@ -19,6 +20,7 @@ impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
auto_hide_toolbar: false,
|
||||
auto_hide_tab_bar: false,
|
||||
show_line_numbers: false,
|
||||
word_wrap: true,
|
||||
theme: Theme::default(),
|
||||
@ -52,7 +54,7 @@ impl Config {
|
||||
if !config_path.exists() {
|
||||
let default_config = Self::default();
|
||||
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;
|
||||
}
|
||||
|
||||
@ -143,7 +143,7 @@ fn get_shortcuts() -> Vec<ShortcutDefinition> {
|
||||
]
|
||||
}
|
||||
|
||||
fn execute_action(action: ShortcutAction, editor: &mut TextEditor, ctx: &egui::Context) -> bool {
|
||||
fn execute_action(action: ShortcutAction, editor: &mut TextEditor, _ctx: &egui::Context) -> bool {
|
||||
match action {
|
||||
ShortcutAction::NewFile => {
|
||||
io::new_file(editor);
|
||||
@ -171,7 +171,9 @@ fn execute_action(action: ShortcutAction, editor: &mut TextEditor, ctx: &egui::C
|
||||
if let Some(current_tab) = editor.get_active_tab() {
|
||||
if current_tab.is_modified {
|
||||
// Show dialog for unsaved changes
|
||||
editor.pending_unsaved_action = Some(super::state::UnsavedAction::CloseTab(editor.active_tab_index));
|
||||
editor.pending_unsaved_action = Some(
|
||||
super::state::UnsavedAction::CloseTab(editor.active_tab_index),
|
||||
);
|
||||
} else {
|
||||
// Close tab directly if no unsaved changes
|
||||
editor.close_tab(editor.active_tab_index);
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
use crate::app::shortcuts;
|
||||
use crate::ui::{
|
||||
about_window::about_window, central_panel::central_panel, find_window::find_window,
|
||||
menu_bar::menu_bar, preferences_window::preferences_window, shortcuts_window::shortcuts_window,
|
||||
tab_bar::tab_bar,
|
||||
};
|
||||
use eframe::egui;
|
||||
|
||||
use super::editor::TextEditor;
|
||||
use crate::app::shortcuts;
|
||||
use crate::ui::about_window::about_window;
|
||||
use crate::ui::central_panel::central_panel;
|
||||
use crate::ui::find_window::find_window;
|
||||
use crate::ui::menu_bar::menu_bar;
|
||||
use crate::ui::preferences_window::preferences_window;
|
||||
use crate::ui::shortcuts_window::shortcuts_window;
|
||||
use crate::ui::tab_bar::tab_bar;
|
||||
|
||||
impl eframe::App for TextEditor {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
@ -24,77 +24,8 @@ impl eframe::App for TextEditor {
|
||||
|
||||
menu_bar(self, ctx);
|
||||
|
||||
// if self.tabs.len() > 1 {
|
||||
if !self.auto_hide_tab_bar {
|
||||
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);
|
||||
@ -117,6 +48,5 @@ impl eframe::App for TextEditor {
|
||||
|
||||
// Update the previous find state for next frame
|
||||
self.prev_show_find = self.show_find;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ impl TextEditor {
|
||||
show_line_numbers: config.show_line_numbers,
|
||||
word_wrap: config.word_wrap,
|
||||
auto_hide_toolbar: config.auto_hide_toolbar,
|
||||
auto_hide_tab_bar: config.auto_hide_tab_bar,
|
||||
theme: config.theme,
|
||||
line_side: config.line_side,
|
||||
font_family: config.font_family,
|
||||
@ -29,23 +30,23 @@ impl TextEditor {
|
||||
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,
|
||||
_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,
|
||||
|
||||
// Track previous content for incremental processing
|
||||
previous_content: String::new(),
|
||||
previous_cursor_char_index: None,
|
||||
current_cursor_line: 0,
|
||||
previous_cursor_line: 0,
|
||||
font_settings_changed: false,
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,8 +73,6 @@ impl TextEditor {
|
||||
|
||||
editor.apply_font_settings(&cc.egui_ctx);
|
||||
|
||||
editor.start_text_processing_thread();
|
||||
|
||||
editor
|
||||
}
|
||||
|
||||
@ -81,6 +80,7 @@ impl TextEditor {
|
||||
Config {
|
||||
auto_hide_toolbar: self.auto_hide_toolbar,
|
||||
show_line_numbers: self.show_line_numbers,
|
||||
auto_hide_tab_bar: self.auto_hide_tab_bar,
|
||||
word_wrap: self.word_wrap,
|
||||
theme: self.theme,
|
||||
line_side: self.line_side,
|
||||
@ -93,7 +93,7 @@ impl TextEditor {
|
||||
pub fn save_config(&self) {
|
||||
let config = self.get_config();
|
||||
if let Err(e) = config.save() {
|
||||
eprintln!("Failed to save configuration: {}", e);
|
||||
eprintln!("Failed to save configuration: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ impl Default for TextEditor {
|
||||
show_line_numbers: false,
|
||||
word_wrap: true,
|
||||
auto_hide_toolbar: false,
|
||||
auto_hide_tab_bar: true,
|
||||
theme: Theme::default(),
|
||||
line_side: false,
|
||||
font_family: "Proportional".to_string(),
|
||||
@ -29,24 +30,23 @@ impl Default for TextEditor {
|
||||
tab_bar_rect: None,
|
||||
menu_bar_stable_until: None,
|
||||
text_processing_result: Arc::new(Mutex::new(TextProcessingResult::default())),
|
||||
processing_thread_handle: None,
|
||||
_processing_thread_handle: None,
|
||||
// Find functionality
|
||||
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: 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,
|
||||
|
||||
// Track previous content for incremental processing
|
||||
previous_content: String::new(),
|
||||
previous_cursor_char_index: None,
|
||||
current_cursor_line: 0,
|
||||
previous_cursor_line: 0,
|
||||
font_settings_changed: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,18 +13,18 @@ pub enum UnsavedAction {
|
||||
#[derive(Clone)]
|
||||
pub struct TextProcessingResult {
|
||||
pub line_count: usize,
|
||||
pub visual_line_mapping: Vec<Option<usize>>,
|
||||
pub max_line_length: f32,
|
||||
pub _processed_content: String,
|
||||
pub longest_line_index: usize, // Which line is the longest (0-based)
|
||||
pub longest_line_length: usize, // Character count of the longest line
|
||||
pub longest_line_pixel_width: f32, // Actual pixel width of the longest line
|
||||
}
|
||||
|
||||
impl Default for TextProcessingResult {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
line_count: 1,
|
||||
visual_line_mapping: vec![Some(1)],
|
||||
max_line_length: 0.0,
|
||||
_processed_content: String::new(),
|
||||
longest_line_index: 0,
|
||||
longest_line_length: 0,
|
||||
longest_line_pixel_width: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -44,6 +44,7 @@ pub struct TextEditor {
|
||||
pub(crate) show_line_numbers: bool,
|
||||
pub(crate) word_wrap: bool,
|
||||
pub(crate) auto_hide_toolbar: bool,
|
||||
pub(crate) auto_hide_tab_bar: bool,
|
||||
pub(crate) theme: Theme,
|
||||
pub(crate) line_side: bool,
|
||||
pub(crate) font_family: String,
|
||||
@ -54,22 +55,20 @@ pub struct TextEditor {
|
||||
pub(crate) tab_bar_rect: Option<egui::Rect>,
|
||||
pub(crate) menu_bar_stable_until: Option<std::time::Instant>,
|
||||
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_matches: Vec<(usize, usize)>, // (start_pos, end_pos) byte positions
|
||||
pub(crate) current_match_index: Option<usize>,
|
||||
pub(crate) case_sensitive_search: bool,
|
||||
pub(crate) prev_show_find: bool, // Track previous state to detect transitions
|
||||
|
||||
// Width calculation cache and state tracking
|
||||
pub(crate) cached_width: Option<f32>,
|
||||
pub(crate) last_word_wrap: bool,
|
||||
pub(crate) last_show_line_numbers: bool,
|
||||
pub(crate) last_font_size: f32,
|
||||
pub(crate) last_line_side: bool,
|
||||
pub(crate) last_viewport_width: f32,
|
||||
// pub(crate) vim_mode: bool,
|
||||
|
||||
// Cursor tracking for smart scrolling
|
||||
pub(crate) previous_cursor_position: Option<usize>,
|
||||
|
||||
// Track previous content for incremental processing
|
||||
pub(crate) previous_content: String,
|
||||
pub(crate) previous_cursor_char_index: Option<usize>,
|
||||
pub(crate) current_cursor_line: usize, // Track current line number incrementally
|
||||
pub(crate) previous_cursor_line: usize, // Track previous line for comparison
|
||||
pub(crate) font_settings_changed: bool, // Flag to trigger text reprocessing when font changes
|
||||
}
|
||||
|
||||
@ -79,7 +79,7 @@ impl TextEditor {
|
||||
ui.add_space(8.0);
|
||||
|
||||
for file in &files_to_list {
|
||||
ui.label(egui::RichText::new(format!("• {}", file)).size(18.0).weak());
|
||||
ui.label(egui::RichText::new(format!("• {file}")).size(18.0).weak());
|
||||
}
|
||||
|
||||
ui.add_space(12.0);
|
||||
|
||||
@ -1,63 +1,331 @@
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
use super::editor::{TextEditor, TextProcessingResult};
|
||||
use eframe::egui;
|
||||
|
||||
impl TextEditor {
|
||||
pub fn start_text_processing_thread(&mut self) {
|
||||
let _processing_result = Arc::clone(&self.text_processing_result);
|
||||
/// Process text content and find the longest line (only used for initial scan)
|
||||
pub fn process_text_for_rendering(&mut self, content: &str, ui: &egui::Ui) {
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
let line_count = lines.len().max(1);
|
||||
|
||||
let handle = thread::Builder::new()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
if lines.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;
|
||||
}
|
||||
|
||||
match handle {
|
||||
Ok(h) => self.processing_thread_handle = Some(h),
|
||||
Err(e) => eprintln!("Failed to start text processing thread: {}", e),
|
||||
let mut longest_line_index = 0;
|
||||
let mut longest_line_length = 0;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_text_for_rendering(
|
||||
&mut self,
|
||||
content: &str,
|
||||
available_width: f32,
|
||||
) {
|
||||
let line_count = content.lines().count().max(1);
|
||||
|
||||
let visual_line_mapping = if self.word_wrap {
|
||||
// For now, simplified mapping - this could be moved to background thread
|
||||
(1..=line_count).map(Some).collect()
|
||||
let font_id = self.get_font_id();
|
||||
let longest_line_pixel_width = if longest_line_length > 0 {
|
||||
let longest_line_text = lines[longest_line_index];
|
||||
ui.fonts(|fonts| {
|
||||
fonts
|
||||
.layout(
|
||||
longest_line_text.to_string(),
|
||||
font_id,
|
||||
egui::Color32::WHITE,
|
||||
f32::INFINITY,
|
||||
)
|
||||
.size()
|
||||
.x
|
||||
})
|
||||
} else {
|
||||
(1..=line_count).map(Some).collect()
|
||||
0.0
|
||||
};
|
||||
|
||||
let result = TextProcessingResult {
|
||||
line_count,
|
||||
visual_line_mapping,
|
||||
max_line_length: available_width,
|
||||
_processed_content: content.to_string(),
|
||||
longest_line_index,
|
||||
longest_line_length,
|
||||
longest_line_pixel_width,
|
||||
};
|
||||
|
||||
if let Ok(mut processing_result) = self.text_processing_result.lock() {
|
||||
*processing_result = result;
|
||||
self.update_processing_result(result);
|
||||
}
|
||||
|
||||
/// Efficiently detect and process line changes without full content iteration
|
||||
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;
|
||||
}
|
||||
|
||||
/// Calculate the change in cursor line without full iteration
|
||||
fn calculate_cursor_line_change(
|
||||
&self,
|
||||
old_content: &str,
|
||||
new_content: &str,
|
||||
old_cursor_pos: usize,
|
||||
new_cursor_pos: usize,
|
||||
) -> isize {
|
||||
let old_newlines = old_content[..old_cursor_pos.min(old_content.len())]
|
||||
.bytes()
|
||||
.filter(|&b| b == b'\n')
|
||||
.count();
|
||||
|
||||
let new_newlines = new_content[..new_cursor_pos.min(new_content.len())]
|
||||
.bytes()
|
||||
.filter(|&b| b == b'\n')
|
||||
.count();
|
||||
|
||||
new_newlines as isize - old_newlines as isize
|
||||
}
|
||||
|
||||
/// Handle character replacement (same length change)
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
/// Handle content addition
|
||||
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;
|
||||
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();
|
||||
|
||||
self.update_line_if_longer(
|
||||
self.current_cursor_line,
|
||||
¤t_line,
|
||||
current_line_length,
|
||||
ui,
|
||||
);
|
||||
}
|
||||
|
||||
/// Handle content removal
|
||||
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);
|
||||
|
||||
if self.current_cursor_line <= current_result.longest_line_index {
|
||||
self.process_text_for_rendering(new_content, ui);
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the current line efficiently without full content scan
|
||||
fn extract_current_line(&self, content: &str, cursor_pos: usize) -> String {
|
||||
let bytes = content.as_bytes();
|
||||
|
||||
let mut line_start = cursor_pos;
|
||||
while line_start > 0 && bytes[line_start - 1] != b'\n' {
|
||||
line_start -= 1;
|
||||
}
|
||||
|
||||
let mut line_end = cursor_pos;
|
||||
while line_end < bytes.len() && bytes[line_end] != b'\n' {
|
||||
line_end += 1;
|
||||
}
|
||||
|
||||
content[line_start..line_end].to_string()
|
||||
}
|
||||
|
||||
/// Update longest line info if the current line is longer
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current text processing result
|
||||
pub fn get_text_processing_result(&self) -> TextProcessingResult {
|
||||
self.text_processing_result
|
||||
.lock()
|
||||
.map(|result| result.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Update the processing result atomically
|
||||
fn update_processing_result(&self, result: TextProcessingResult) {
|
||||
if let Ok(mut processing_result) = self.text_processing_result.lock() {
|
||||
*processing_result = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,14 +12,19 @@ impl TextEditor {
|
||||
pub fn get_title(&self) -> String {
|
||||
if let Some(tab) = self.get_active_tab() {
|
||||
let modified_indicator = if tab.is_modified { "*" } else { "" };
|
||||
format!("{}{} - C-Text", tab.title, modified_indicator)
|
||||
format!(
|
||||
"{}{} - {}",
|
||||
tab.title,
|
||||
modified_indicator,
|
||||
env!("CARGO_PKG_NAME")
|
||||
)
|
||||
} else {
|
||||
"C-Text".to_string()
|
||||
format!("{} - {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the configured font ID based on the editor's font settings
|
||||
fn get_font_id(&self) -> egui::FontId {
|
||||
pub fn get_font_id(&self) -> egui::FontId {
|
||||
let font_family = match self.font_family.as_str() {
|
||||
"Monospace" => egui::FontFamily::Monospace,
|
||||
_ => egui::FontFamily::Proportional,
|
||||
@ -27,11 +32,13 @@ impl TextEditor {
|
||||
egui::FontId::new(self.font_size, font_family)
|
||||
}
|
||||
|
||||
/// Immediately apply theme and save to configuration
|
||||
pub fn set_theme(&mut self, ctx: &egui::Context) {
|
||||
theme::apply(self.theme, ctx);
|
||||
self.save_config();
|
||||
}
|
||||
|
||||
/// Apply font settings with immediate text reprocessing
|
||||
pub fn apply_font_settings(&mut self, ctx: &egui::Context) {
|
||||
let font_family = match self.font_family.as_str() {
|
||||
"Monospace" => egui::FontFamily::Monospace,
|
||||
@ -45,9 +52,27 @@ impl TextEditor {
|
||||
);
|
||||
|
||||
ctx.set_style(style);
|
||||
self.font_settings_changed = true;
|
||||
self.save_config();
|
||||
}
|
||||
|
||||
/// Apply font settings with immediate text reprocessing
|
||||
pub fn apply_font_settings_with_ui(&mut self, ctx: &egui::Context, ui: &egui::Ui) {
|
||||
self.apply_font_settings(ctx);
|
||||
self.reprocess_text_for_font_change(ui);
|
||||
self.font_settings_changed = false;
|
||||
}
|
||||
|
||||
/// Trigger immediate text reprocessing when font settings change
|
||||
pub fn reprocess_text_for_font_change(&mut self, ui: &egui::Ui) {
|
||||
if let Some(active_tab) = self.get_active_tab() {
|
||||
let content = active_tab.content.clone();
|
||||
if !content.is_empty() {
|
||||
self.process_text_for_rendering(&content, ui);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the available width for the text editor, accounting for line numbers and separator
|
||||
pub fn calculate_editor_dimensions(&self, ui: &egui::Ui) -> EditorDimensions {
|
||||
let total_available_width = ui.available_width();
|
||||
@ -60,11 +85,9 @@ impl TextEditor {
|
||||
};
|
||||
}
|
||||
|
||||
// Get line count from processing result
|
||||
let processing_result = self.get_text_processing_result();
|
||||
let line_count = processing_result.line_count;
|
||||
|
||||
// Calculate base line number width
|
||||
let font_id = self.get_font_id();
|
||||
let line_count_digits = line_count.to_string().len();
|
||||
let sample_text = "9".repeat(line_count_digits);
|
||||
@ -75,11 +98,10 @@ impl TextEditor {
|
||||
.x
|
||||
});
|
||||
|
||||
// Add padding based on line_side setting
|
||||
let line_number_width = if self.line_side {
|
||||
base_line_number_width + 20.0 // Extra padding when line numbers are on the side
|
||||
base_line_number_width + 20.0
|
||||
} else {
|
||||
base_line_number_width + 8.0 // Minimal padding when line numbers are normal
|
||||
base_line_number_width + 8.0
|
||||
};
|
||||
|
||||
// Separator space (7.0 for separator + 3.0 spacing = 10.0 total)
|
||||
@ -95,68 +117,17 @@ impl TextEditor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the available width for non-word-wrapped content based on content analysis
|
||||
/// 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 {
|
||||
if let Some(active_tab) = self.get_active_tab() {
|
||||
let content = &active_tab.content;
|
||||
let processing_result = self.get_text_processing_result();
|
||||
|
||||
if content.is_empty() {
|
||||
if processing_result.longest_line_length == 0 {
|
||||
return self.calculate_editor_dimensions(ui).text_width;
|
||||
}
|
||||
|
||||
// Find the longest line
|
||||
let longest_line = content
|
||||
.lines()
|
||||
.max_by_key(|line| line.chars().count())
|
||||
.unwrap_or("");
|
||||
let longest_line_width = processing_result.longest_line_pixel_width + self.font_size;
|
||||
|
||||
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);
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ pub struct Tab {
|
||||
pub file_path: Option<PathBuf>,
|
||||
pub is_modified: bool,
|
||||
pub title: String,
|
||||
hasher: DefaultHasher,
|
||||
pub hasher: DefaultHasher,
|
||||
}
|
||||
|
||||
impl Tab {
|
||||
@ -29,7 +29,7 @@ impl Tab {
|
||||
content,
|
||||
file_path: None,
|
||||
is_modified: false,
|
||||
title: format!("new_{}", tab_number),
|
||||
title: format!("new_{tab_number}"),
|
||||
hasher,
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,7 +77,7 @@ fn get_pywal_colors() -> Option<egui::Visuals> {
|
||||
let fg = parse_color(colors.get(7).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 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) {
|
||||
egui::Visuals::dark()
|
||||
|
||||
@ -43,7 +43,7 @@ pub(crate) fn open_file(app: &mut TextEditor) {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Failed to open file: {}", err);
|
||||
eprintln!("Failed to open file: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -81,7 +81,7 @@ pub(crate) fn save_to_path(app: &mut TextEditor, path: PathBuf) {
|
||||
active_tab.mark_as_saved();
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Failed to save file: {}", err);
|
||||
eprintln!("Failed to save file: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,21 +5,21 @@ use eframe::egui;
|
||||
mod app;
|
||||
mod io;
|
||||
mod ui;
|
||||
use app::{TextEditor, config::Config};
|
||||
use app::{config::Config, TextEditor};
|
||||
|
||||
fn main() -> eframe::Result {
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_min_inner_size([600.0, 400.0])
|
||||
.with_title("C-Ext")
|
||||
.with_app_id("io.lampnet.c-ext"),
|
||||
.with_title("ced")
|
||||
.with_app_id("io.lampnet.ced"),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let config = Config::load();
|
||||
|
||||
eframe::run_native(
|
||||
"C-Ext",
|
||||
"ced",
|
||||
options,
|
||||
Box::new(move |cc| Ok(Box::new(TextEditor::from_config_with_context(config, cc)))),
|
||||
)
|
||||
|
||||
@ -14,7 +14,7 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
let line_side = app.line_side;
|
||||
let font_size = app.font_size;
|
||||
|
||||
egui::CentralPanel::default()
|
||||
let _output = egui::CentralPanel::default()
|
||||
.frame(egui::Frame::NONE)
|
||||
.show(ctx, |ui| {
|
||||
let bg_color = ui.visuals().extreme_bg_color;
|
||||
@ -23,11 +23,20 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
let editor_height = panel_rect.height();
|
||||
|
||||
if !show_line_numbers || app.get_active_tab().is_none() {
|
||||
let _scroll_response =
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false; 2])
|
||||
.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);
|
||||
});
|
||||
|
||||
show_context_menu(ui, app, &context_response);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@ -77,7 +86,8 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
.show(ui, |ui| {
|
||||
if line_side {
|
||||
// Line numbers on the right
|
||||
let text_editor_width = 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(
|
||||
egui::vec2(text_editor_width, editor_height),
|
||||
egui::Layout::left_to_right(egui::Align::TOP),
|
||||
@ -86,27 +96,111 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
ui.allocate_ui_with_layout(
|
||||
egui::vec2(editor_dimensions.text_width, editor_height),
|
||||
egui::Layout::left_to_right(egui::Align::TOP),
|
||||
|ui| {
|
||||
// Create an invisible interaction area for context menu
|
||||
let full_rect = ui.available_rect_before_wrap();
|
||||
let context_response = ui.allocate_response(
|
||||
full_rect.size(),
|
||||
egui::Sense::click(),
|
||||
);
|
||||
|
||||
// Reset cursor to render editor at the top
|
||||
ui.scope_builder(
|
||||
egui::UiBuilder::new().max_rect(full_rect),
|
||||
|ui| {
|
||||
editor_view_ui(ui, app);
|
||||
},
|
||||
);
|
||||
|
||||
show_context_menu(ui, app, &context_response);
|
||||
},
|
||||
);
|
||||
separator_widget(ui);
|
||||
line_numbers_widget(ui);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// Line numbers on the left
|
||||
let text_editor_width = 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(
|
||||
egui::vec2(text_editor_width, editor_height),
|
||||
egui::Layout::left_to_right(egui::Align::TOP),
|
||||
|ui| {
|
||||
line_numbers_widget(ui);
|
||||
separator_widget(ui);
|
||||
|
||||
// Create an invisible interaction area for context menu
|
||||
let editor_area = ui.available_rect_before_wrap();
|
||||
let context_response =
|
||||
ui.allocate_response(editor_area.size(), egui::Sense::click());
|
||||
|
||||
// Reset cursor to render editor at the current position
|
||||
ui.scope_builder(
|
||||
egui::UiBuilder::new().max_rect(editor_area),
|
||||
|ui| {
|
||||
editor_view_ui(ui, app);
|
||||
},
|
||||
);
|
||||
|
||||
show_context_menu(ui, app, &context_response);
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn show_context_menu(_ui: &mut egui::Ui, app: &mut TextEditor, context_response: &egui::Response) {
|
||||
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_menu();
|
||||
}
|
||||
if ui.button("Copy").clicked() {
|
||||
ui.ctx()
|
||||
.send_viewport_cmd(egui::ViewportCommand::RequestCopy);
|
||||
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,
|
||||
})
|
||||
});
|
||||
ui.close_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_menu();
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("Reset Zoom").clicked() {
|
||||
ui.ctx().memory_mut(|mem| {
|
||||
mem.data.insert_temp(reset_zoom_key, true);
|
||||
});
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,28 +1,21 @@
|
||||
use crate::app::TextEditor;
|
||||
use eframe::egui;
|
||||
|
||||
use super::find_highlight::draw_find_highlight;
|
||||
|
||||
pub(super) fn editor_view(
|
||||
ui: &mut egui::Ui,
|
||||
app: &mut TextEditor,
|
||||
) -> (egui::Response, Option<egui::Rect>) {
|
||||
let current_match_position = app.get_current_match_position();
|
||||
pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::Response {
|
||||
let _current_match_position = app.get_current_match_position();
|
||||
let show_find = app.show_find;
|
||||
let prev_show_find = app.prev_show_find;
|
||||
let _prev_show_find = app.prev_show_find;
|
||||
let show_preferences = app.show_preferences;
|
||||
let show_about = app.show_about;
|
||||
let show_shortcuts = app.show_shortcuts;
|
||||
let word_wrap = app.word_wrap;
|
||||
let font_size = app.font_size;
|
||||
|
||||
// Check if reset zoom was requested in previous frame
|
||||
let reset_zoom_key = egui::Id::new("editor_reset_zoom");
|
||||
let should_reset_zoom = ui.ctx().memory_mut(|mem| {
|
||||
mem.data.get_temp::<bool>(reset_zoom_key).unwrap_or(false)
|
||||
});
|
||||
let should_reset_zoom = ui
|
||||
.ctx()
|
||||
.memory_mut(|mem| mem.data.get_temp::<bool>(reset_zoom_key).unwrap_or(false));
|
||||
|
||||
// Reset zoom if requested
|
||||
if should_reset_zoom {
|
||||
app.zoom_factor = 1.0;
|
||||
ui.ctx().set_zoom_factor(1.0);
|
||||
@ -31,7 +24,16 @@ pub(super) fn editor_view(
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(active_tab) = app.get_active_tab_mut() {
|
||||
let estimated_width = if !word_wrap {
|
||||
app.calculate_content_based_width(ui)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
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 editor_rect = ui.available_rect_before_wrap();
|
||||
ui.painter().rect_filled(editor_rect, 0.0, bg_color);
|
||||
@ -52,65 +54,96 @@ pub(super) fn editor_view(
|
||||
.cursor_at_end(false)
|
||||
.id(egui::Id::new("main_text_editor"));
|
||||
|
||||
let output = text_edit.show(ui);
|
||||
|
||||
// Store text length for context menu
|
||||
let text_len = active_tab.content.len();
|
||||
|
||||
// Right-click context menu
|
||||
output.response.context_menu(|ui| {
|
||||
if ui.button("Cut").clicked() {
|
||||
ui.ctx().input_mut(|i| i.events.push(egui::Event::Cut));
|
||||
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,
|
||||
let output = if word_wrap {
|
||||
text_edit.show(ui)
|
||||
} else {
|
||||
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| text_edit.show(ui),
|
||||
)
|
||||
})
|
||||
});
|
||||
ui.close_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),
|
||||
.inner
|
||||
.inner
|
||||
};
|
||||
|
||||
let content_changed = output.response.changed();
|
||||
let content_for_processing = if content_changed {
|
||||
active_tab.update_modified_state();
|
||||
Some(active_tab.content.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
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.clone();
|
||||
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);
|
||||
}
|
||||
ui.close_menu();
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("Reset Zoom").clicked() {
|
||||
ui.ctx().memory_mut(|mem| {
|
||||
mem.data.insert_temp(reset_zoom_key, true);
|
||||
});
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
app.process_text_for_rendering(&content, ui);
|
||||
|
||||
let cursor_rect = if let Some(cursor_range) = output.state.cursor.char_range() {
|
||||
let cursor_pos = cursor_range.primary.index;
|
||||
if let Some(cursor_pos) = current_cursor_pos {
|
||||
app.current_cursor_line = content[..cursor_pos]
|
||||
.bytes()
|
||||
.filter(|&b| b == b'\n')
|
||||
.count();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.previous_content = content.clone();
|
||||
app.previous_cursor_char_index = current_cursor_pos;
|
||||
|
||||
if let Some(active_tab) = app.get_active_tab_mut() {
|
||||
active_tab.last_content_hash =
|
||||
crate::app::tab::compute_content_hash(&active_tab.content, &mut active_tab.hasher);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if font settings changed and trigger reprocessing
|
||||
if app.font_settings_changed {
|
||||
if let Some(active_tab) = app.get_active_tab() {
|
||||
let content = active_tab.content.clone();
|
||||
if !content.is_empty() {
|
||||
app.process_text_for_rendering(&content, ui);
|
||||
}
|
||||
}
|
||||
app.font_settings_changed = 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 text_up_to_cursor = &content[..cursor_pos.min(content.len())];
|
||||
let cursor_line = text_up_to_cursor.chars().filter(|&c| c == '\n').count();
|
||||
let cursor_line = content
|
||||
.char_indices()
|
||||
.take_while(|(byte_pos, _)| *byte_pos < cursor_pos)
|
||||
.filter(|(_, ch)| *ch == '\n')
|
||||
.count();
|
||||
|
||||
let font_id = ui
|
||||
.style()
|
||||
@ -125,44 +158,19 @@ pub(super) fn editor_view(
|
||||
egui::pos2(output.response.rect.left(), y_pos),
|
||||
egui::vec2(2.0, line_height),
|
||||
);
|
||||
Some(cursor_rect)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if !show_find && prev_show_find {
|
||||
if let Some((start_pos, end_pos)) = current_match_position {
|
||||
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);
|
||||
let visible_area = ui.clip_rect();
|
||||
if !visible_area.intersects(cursor_rect) {
|
||||
ui.scroll_to_rect(cursor_rect, Some(egui::Align::Center));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
app.previous_cursor_position = Some(cursor_pos);
|
||||
}
|
||||
}
|
||||
|
||||
if output.response.changed() {
|
||||
active_tab.update_modified_state();
|
||||
app.find_matches.clear();
|
||||
app.current_match_index = None;
|
||||
}
|
||||
|
||||
// Request focus if no dialogs are open
|
||||
if !output.response.has_focus()
|
||||
&& !show_preferences
|
||||
&& !show_about
|
||||
@ -171,52 +179,6 @@ pub(super) fn editor_view(
|
||||
{
|
||||
output.response.request_focus();
|
||||
}
|
||||
(output.response, cursor_rect)
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
output.response
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use eframe::egui;
|
||||
|
||||
pub(super) fn draw_find_highlight(
|
||||
pub(super) fn _draw_find_highlight(
|
||||
ui: &mut egui::Ui,
|
||||
content: &str,
|
||||
start_pos: usize,
|
||||
@ -74,10 +74,7 @@ pub(super) fn draw_find_highlight(
|
||||
egui::vec2(match_width, line_height),
|
||||
);
|
||||
|
||||
ui.painter().rect_filled(
|
||||
highlight_rect,
|
||||
0.0,
|
||||
ui.visuals().selection.bg_fill,
|
||||
);
|
||||
ui.painter()
|
||||
.rect_filled(highlight_rect, 0.0, ui.visuals().selection.bg_fill);
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,8 +86,7 @@ pub(super) fn render_line_numbers(
|
||||
let bg_color = ui.visuals().extreme_bg_color;
|
||||
|
||||
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 line_count_width = line_count.to_string().len();
|
||||
|
||||
@ -11,12 +11,12 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
|
||||
let should_show_menubar = !app.auto_hide_toolbar || {
|
||||
if app.menu_interaction_active {
|
||||
app.menu_bar_stable_until = Some(now + std::time::Duration::from_millis(500));
|
||||
app.menu_bar_stable_until = Some(now + std::time::Duration::from_millis(16));
|
||||
true
|
||||
} else if should_stay_stable {
|
||||
true
|
||||
} else if let Some(pointer_pos) = ctx.pointer_hover_pos() {
|
||||
let in_menu_trigger_area = pointer_pos.y < 10.0;
|
||||
let in_menu_trigger_area = pointer_pos.y < 5.0;
|
||||
|
||||
if in_menu_trigger_area {
|
||||
app.menu_bar_stable_until = Some(now + std::time::Duration::from_millis(300));
|
||||
@ -172,14 +172,18 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
ui.menu_button("View", |ui| {
|
||||
app.menu_interaction_active = true;
|
||||
if ui
|
||||
.checkbox(&mut app.show_line_numbers, "Toggle Line Numbers")
|
||||
.checkbox(&mut app.show_line_numbers, "Show Line Numbers")
|
||||
.clicked()
|
||||
{
|
||||
app.save_config();
|
||||
ui.close_menu();
|
||||
}
|
||||
if ui.checkbox(&mut app.word_wrap, "Word Wrap").clicked() {
|
||||
app.save_config();
|
||||
ui.close_menu();
|
||||
}
|
||||
if ui
|
||||
.checkbox(&mut app.word_wrap, "Toggle Word Wrap")
|
||||
.checkbox(&mut app.auto_hide_tab_bar, "Hide Tab Bar")
|
||||
.clicked()
|
||||
{
|
||||
app.save_config();
|
||||
@ -265,6 +269,48 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
|
||||
if app.auto_hide_tab_bar {
|
||||
let tab_title = if let Some(tab) = app.get_active_tab() {
|
||||
tab.title.clone()
|
||||
} else {
|
||||
let empty_tab = crate::app::tab::Tab::new_empty(1);
|
||||
empty_tab.title.clone()
|
||||
};
|
||||
|
||||
let window_width = ctx.screen_rect().width();
|
||||
let font_id = ui.style().text_styles[&egui::TextStyle::Body].clone();
|
||||
|
||||
let tab_title = if app.get_active_tab().is_some_and(|tab| tab.is_modified) {
|
||||
format!("{tab_title}*")
|
||||
} else {
|
||||
tab_title
|
||||
};
|
||||
|
||||
let text_galley = ui.fonts(|fonts| {
|
||||
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());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -58,7 +58,7 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
});
|
||||
|
||||
if changed {
|
||||
app.apply_font_settings(ctx);
|
||||
app.apply_font_settings_with_ui(ctx, ui);
|
||||
}
|
||||
});
|
||||
|
||||
@ -72,7 +72,11 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
app.font_size_input = Some(app.font_size.to_string());
|
||||
}
|
||||
|
||||
let mut font_size_text = app.font_size_input.as_ref().unwrap().clone();
|
||||
let mut font_size_text = app
|
||||
.font_size_input
|
||||
.as_ref()
|
||||
.unwrap_or(&"14".to_string())
|
||||
.clone();
|
||||
let response = ui.add(
|
||||
egui::TextEdit::singleline(&mut font_size_text)
|
||||
.desired_width(50.0)
|
||||
@ -93,7 +97,7 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
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_with_ui(ctx, ui);
|
||||
}
|
||||
}
|
||||
app.font_size_input = None;
|
||||
@ -123,7 +127,9 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
},
|
||||
);
|
||||
ui.label(
|
||||
egui::RichText::new("The quick brown fox jumps over the lazy dog.")
|
||||
egui::RichText::new(
|
||||
"The quick brown fox jumps over the lazy dog.",
|
||||
)
|
||||
.font(preview_font.clone()),
|
||||
);
|
||||
ui.label(
|
||||
@ -134,7 +140,9 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
egui::RichText::new("abcdefghijklmnopqrstuvwxyz")
|
||||
.font(preview_font.clone()),
|
||||
);
|
||||
ui.label(egui::RichText::new("1234567890 !@#$%^&*()").font(preview_font));
|
||||
ui.label(
|
||||
egui::RichText::new("1234567890 !@#$%^&*()").font(preview_font),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -27,9 +27,7 @@ fn render_shortcuts_content(ui: &mut egui::Ui) {
|
||||
ui.separator();
|
||||
ui.label(egui::RichText::new("Views").size(18.0).strong());
|
||||
ui.label(egui::RichText::new("Ctrl + L: Toggle Line Numbers").size(14.0));
|
||||
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(14.0));
|
||||
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 + P: Preferences").size(14.0));
|
||||
@ -53,8 +51,8 @@ pub(crate) fn shortcuts_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
let screen_rect = ctx.screen_rect();
|
||||
|
||||
// Calculate appropriate window size that always fits nicely in the main window
|
||||
let window_width = (screen_rect.width() * 0.6).min(400.0).max(300.0);
|
||||
let window_height = (screen_rect.height() * 0.7).min(500.0).max(250.0);
|
||||
let window_width = (screen_rect.width() * 0.6).clamp(300.0, 400.0);
|
||||
let window_height = (screen_rect.height() * 0.7).clamp(250.0, 500.0);
|
||||
|
||||
egui::Window::new("Shortcuts")
|
||||
.collapsible(false)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user