pub mod highlighting; mod syntax; mod themes; #[cfg(feature = "egui")] use egui::text::LayoutJob; #[cfg(feature = "egui")] use egui::widgets::text_edit::TextEditOutput; pub use highlighting::Token; #[cfg(feature = "egui")] use highlighting::highlight; #[cfg(feature = "editor")] use std::hash::{Hash, Hasher}; pub use syntax::{Syntax, TokenType}; pub use themes::ColorTheme; pub use themes::DEFAULT_THEMES; #[cfg(feature = "egui")] pub trait Editor: Hash { fn append(&self, job: &mut LayoutJob, token: &Token); fn syntax(&self) -> &Syntax; } #[cfg(feature = "editor")] #[derive(Clone, Debug, PartialEq)] /// CodeEditor struct which stores settings for highlighting. pub struct CodeEditor { id: String, theme: ColorTheme, syntax: Syntax, numlines: bool, numlines_shift: isize, numlines_only_natural: bool, fontsize: f32, rows: usize, stick_to_bottom: bool, desired_width: f32, } #[cfg(feature = "editor")] impl Hash for CodeEditor { fn hash(&self, state: &mut H) { self.theme.hash(state); #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] (self.fontsize as u32).hash(state); self.syntax.hash(state); } } #[cfg(feature = "editor")] impl Default for CodeEditor { fn default() -> CodeEditor { CodeEditor { id: String::from("Code Editor"), theme: ColorTheme::THEME, syntax: Syntax::dsa(), numlines: true, numlines_shift: 0, numlines_only_natural: false, fontsize: 10.0, rows: 10, stick_to_bottom: false, desired_width: f32::INFINITY, } } } #[cfg(feature = "editor")] impl CodeEditor { pub fn id_source(self, id_source: impl Into) -> Self { CodeEditor { id: id_source.into(), ..self } } /// Minimum number of rows to show. /// /// **Default: 10** pub fn with_rows(self, rows: usize) -> Self { CodeEditor { rows, ..self } } /// Use custom Color Theme /// /// **Default: Gruvbox** pub fn with_theme(self, theme: ColorTheme) -> Self { CodeEditor { theme, ..self } } /// Use custom font size /// /// **Default: 10.0** pub fn with_fontsize(self, fontsize: f32) -> Self { CodeEditor { fontsize, ..self } } #[cfg(feature = "egui")] /// 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 } } /// Use custom syntax for highlighting /// /// **Default: Rust** pub fn with_syntax(self, syntax: Syntax) -> Self { CodeEditor { syntax, ..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 } } #[cfg(feature = "egui")] pub fn format(&self, ty: TokenType) -> egui::text::TextFormat { let font_id = egui::FontId::monospace(self.fontsize); let color = self.theme.type_color(ty); egui::text::TextFormat::simple(font_id, color) } #[cfg(feature = "egui")] 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() } .max(self.rows) 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::>() .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), self.theme.type_color(TokenType::Comment(true)), ), ); 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_rows(self.rows) .desired_width(width) .layouter(&mut layouter), ); } #[cfg(feature = "egui")] /// Show Code Editor pub fn show( &mut self, ui: &mut egui::Ui, text: &mut dyn egui::TextBuffer, ) -> TextEditOutput { let mut text_edit_output: Option = None; let code_editor = |ui: &mut egui::Ui| { ui.horizontal_top(|h| { self.theme.modify_style(h, self.fontsize); 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 mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| { let layout_job = highlight(ui.ctx(), self, string); ui.fonts(|f| f.layout_job(layout_job)) }; let output = egui::TextEdit::multiline(text) .id_source(&self.id) .lock_focus(true) .desired_rows(self.rows) .frame(false) .desired_width(self.desired_width) .layouter(&mut layouter) .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") } } #[cfg(feature = "editor")] #[cfg(feature = "egui")] impl Editor for CodeEditor { fn append(&self, job: &mut LayoutJob, token: &Token) { job.append(token.buffer(), 0.0, self.format(token.ty())); } fn syntax(&self) -> &Syntax { &self.syntax } }