223 lines
6.8 KiB
Rust
223 lines
6.8 KiB
Rust
|
|
|
|
use egui::{text::LayoutJob, Color32};
|
|
use egui::widgets::text_edit::TextEditOutput;
|
|
|
|
use std::hash::{Hash, Hasher};
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
/// CodeEditor struct which stores settings for highlighting.
|
|
pub struct CodeEditor {
|
|
id: String,
|
|
numlines: bool,
|
|
numlines_shift: isize,
|
|
numlines_only_natural: bool,
|
|
fontsize: f32,
|
|
stick_to_bottom: bool,
|
|
desired_width: f32,
|
|
}
|
|
|
|
impl Hash for CodeEditor {
|
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
|
(self.fontsize as u32).hash(state);
|
|
}
|
|
}
|
|
|
|
impl Default for CodeEditor {
|
|
fn default() -> CodeEditor {
|
|
CodeEditor {
|
|
id: String::from("Code Editor"),
|
|
numlines: true,
|
|
numlines_shift: 0,
|
|
numlines_only_natural: false,
|
|
fontsize: 10.0,
|
|
stick_to_bottom: false,
|
|
desired_width: f32::INFINITY,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CodeEditor {
|
|
pub fn id_source(self, id_source: impl Into<String>) -> Self {
|
|
CodeEditor {
|
|
id: id_source.into(),
|
|
..self
|
|
}
|
|
}
|
|
|
|
/// Use custom font size
|
|
///
|
|
/// **Default: 10.0**
|
|
pub fn with_fontsize(self, fontsize: f32) -> Self {
|
|
CodeEditor { fontsize, ..self }
|
|
}
|
|
|
|
/// Use UI font size
|
|
pub fn with_ui_fontsize(self, ui: &mut egui::Ui) -> Self {
|
|
CodeEditor {
|
|
fontsize: egui::TextStyle::Monospace.resolve(ui.style()).size,
|
|
..self
|
|
}
|
|
}
|
|
|
|
/// Show or hide lines numbering
|
|
///
|
|
/// **Default: true**
|
|
pub fn with_numlines(self, numlines: bool) -> Self {
|
|
CodeEditor { numlines, ..self }
|
|
}
|
|
|
|
/// Shift lines numbering by this value
|
|
///
|
|
/// **Default: 0**
|
|
pub fn with_numlines_shift(self, numlines_shift: isize) -> Self {
|
|
CodeEditor {
|
|
numlines_shift,
|
|
..self
|
|
}
|
|
}
|
|
|
|
/// Show lines numbering only above zero, useful for enabling numbering since nth row
|
|
///
|
|
/// **Default: false**
|
|
pub fn with_numlines_only_natural(self, numlines_only_natural: bool) -> Self {
|
|
CodeEditor {
|
|
numlines_only_natural,
|
|
..self
|
|
}
|
|
}
|
|
|
|
/// Should the containing area shrink if the content is small?
|
|
///
|
|
/// **Default: false**
|
|
pub fn auto_shrink(self, shrink: bool) -> Self {
|
|
CodeEditor {
|
|
desired_width: if shrink { 0.0 } else { self.desired_width },
|
|
..self
|
|
}
|
|
}
|
|
|
|
/// Sets the desired width of the code editor
|
|
///
|
|
/// **Default: `f32::INFINITY`**
|
|
pub fn desired_width(self, width: f32) -> Self {
|
|
CodeEditor {
|
|
desired_width: width,
|
|
..self
|
|
}
|
|
}
|
|
|
|
/// Stick to bottom
|
|
/// The scroll handle will stick to the bottom position even while the content size
|
|
/// changes dynamically. This can be useful to simulate terminal UIs or log/info scrollers.
|
|
/// The scroll handle remains stuck until user manually changes position. Once "unstuck"
|
|
/// it will remain focused on whatever content viewport the user left it on. If the scroll
|
|
/// handle is dragged to the bottom it will again become stuck and remain there until manually
|
|
/// pulled from the end position.
|
|
///
|
|
/// **Default: false**
|
|
pub fn stick_to_bottom(self, stick_to_bottom: bool) -> Self {
|
|
CodeEditor {
|
|
stick_to_bottom,
|
|
..self
|
|
}
|
|
}
|
|
|
|
pub fn format(&self, string: &str) -> egui::text::TextFormat {
|
|
let font_id = egui::FontId::monospace(self.fontsize);
|
|
let color = Color32::WHITE;
|
|
egui::text::TextFormat::simple(font_id, color)
|
|
}
|
|
|
|
fn numlines_show(&self, ui: &mut egui::Ui, text: &str) {
|
|
let total = if text.ends_with('\n') || text.is_empty() {
|
|
text.lines().count() + 1
|
|
} else {
|
|
text.lines().count()
|
|
} as isize;
|
|
|
|
let max_indent = total.to_string().len().max(
|
|
!self.numlines_only_natural as usize * self.numlines_shift.to_string().len(),
|
|
);
|
|
let mut counter = (1..=total)
|
|
.map(|i| {
|
|
let num = i + self.numlines_shift;
|
|
if num <= 0 && self.numlines_only_natural {
|
|
String::new()
|
|
} else {
|
|
let label = num.to_string();
|
|
format!(
|
|
"{}{label}",
|
|
" ".repeat(max_indent.saturating_sub(label.len()))
|
|
)
|
|
}
|
|
})
|
|
.collect::<Vec<String>>()
|
|
.join("\n");
|
|
|
|
#[allow(clippy::cast_precision_loss)]
|
|
let width = max_indent as f32
|
|
* self.fontsize
|
|
* 0.5
|
|
* !(total + self.numlines_shift <= 0 && self.numlines_only_natural) as u8
|
|
as f32;
|
|
|
|
let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| {
|
|
let layout_job = egui::text::LayoutJob::single_section(
|
|
string.to_string(),
|
|
egui::TextFormat::simple(
|
|
egui::FontId::monospace(self.fontsize),
|
|
Color32::WHITE,
|
|
),
|
|
);
|
|
ui.fonts(|f| f.layout_job(layout_job))
|
|
};
|
|
|
|
ui.add(
|
|
egui::TextEdit::multiline(&mut counter)
|
|
.id_source(format!("{}_numlines", self.id))
|
|
.font(egui::TextStyle::Monospace)
|
|
.interactive(false)
|
|
.frame(false)
|
|
.desired_width(width)
|
|
.layouter(&mut layouter),
|
|
);
|
|
}
|
|
|
|
/// Show Code Editor
|
|
pub fn show(
|
|
&mut self,
|
|
ui: &mut egui::Ui,
|
|
text: &mut dyn egui::TextBuffer,
|
|
) -> TextEditOutput {
|
|
let mut text_edit_output: Option<TextEditOutput> = None;
|
|
let code_editor = |ui: &mut egui::Ui| {
|
|
ui.horizontal_top(|h| {
|
|
if self.numlines {
|
|
self.numlines_show(h, text.as_str());
|
|
}
|
|
egui::ScrollArea::horizontal()
|
|
.hscroll(true)
|
|
.id_salt(format!("{}_inner_scroll", self.id))
|
|
.show(h, |ui| {
|
|
let output = egui::TextEdit::multiline(text)
|
|
.id_source(&self.id)
|
|
.lock_focus(true)
|
|
.frame(false)
|
|
.desired_width(self.desired_width)
|
|
.show(ui);
|
|
text_edit_output = Some(output);
|
|
});
|
|
});
|
|
};
|
|
|
|
egui::ScrollArea::vertical()
|
|
.id_salt(format!("{}_outer_scroll", self.id))
|
|
.stick_to_bottom(self.stick_to_bottom)
|
|
.show(ui, code_editor);
|
|
|
|
text_edit_output.expect("TextEditOutput should exist at this point")
|
|
}
|
|
}
|