1#![allow(rustdoc::invalid_rust_codeblocks)]
2#[cfg(feature = "egui")]
71mod completer;
72pub mod highlighting;
73mod syntax;
74#[cfg(test)]
75mod tests;
76mod themes;
77
78#[cfg(feature = "egui")]
79use egui::text::LayoutJob;
80#[cfg(feature = "egui")]
81use egui::widgets::text_edit::TextEditOutput;
82pub use highlighting::Token;
83#[cfg(feature = "egui")]
84use highlighting::highlight;
85#[cfg(feature = "editor")]
86use std::hash::{Hash, Hasher};
87pub use syntax::{Syntax, TokenType};
88pub use themes::ColorTheme;
89pub use themes::DEFAULT_THEMES;
90
91#[cfg(feature = "egui")]
92pub use crate::completer::Completer;
93
94#[cfg(feature = "egui")]
95pub trait Editor: Hash {
96 fn append(&self, job: &mut LayoutJob, token: &Token);
97 fn syntax(&self) -> &Syntax;
98}
99
100#[cfg(feature = "editor")]
101#[derive(Clone, Debug, PartialEq)]
102pub struct CodeEditor {
104 id: String,
105 theme: ColorTheme,
106 syntax: Syntax,
107 numlines: bool,
108 numlines_shift: isize,
109 numlines_only_natural: bool,
110 fontsize: f32,
111 rows: usize,
112 vscroll: bool,
113 stick_to_bottom: bool,
114 desired_width: f32,
115}
116
117#[cfg(feature = "editor")]
118impl Hash for CodeEditor {
119 fn hash<H: Hasher>(&self, state: &mut H) {
120 self.theme.hash(state);
121 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
122 (self.fontsize as u32).hash(state);
123 self.syntax.hash(state);
124 }
125}
126
127#[cfg(feature = "editor")]
128impl Default for CodeEditor {
129 fn default() -> CodeEditor {
130 let syntax = Syntax::rust();
131 CodeEditor {
132 id: String::from("Code Editor"),
133 theme: ColorTheme::GRUVBOX,
134 syntax,
135 numlines: true,
136 numlines_shift: 0,
137 numlines_only_natural: false,
138 fontsize: 10.0,
139 rows: 10,
140 vscroll: true,
141 stick_to_bottom: false,
142 desired_width: f32::INFINITY,
143 }
144 }
145}
146
147#[cfg(feature = "editor")]
148impl CodeEditor {
149 pub fn id_source(self, id_source: impl Into<String>) -> Self {
150 CodeEditor {
151 id: id_source.into(),
152 ..self
153 }
154 }
155
156 pub fn with_rows(self, rows: usize) -> Self {
160 CodeEditor { rows, ..self }
161 }
162
163 pub fn with_theme(self, theme: ColorTheme) -> Self {
167 CodeEditor { theme, ..self }
168 }
169
170 pub fn with_fontsize(self, fontsize: f32) -> Self {
174 CodeEditor { fontsize, ..self }
175 }
176
177 #[cfg(feature = "egui")]
178 pub fn with_ui_fontsize(self, ui: &mut egui::Ui) -> Self {
180 CodeEditor {
181 fontsize: egui::TextStyle::Monospace.resolve(ui.style()).size,
182 ..self
183 }
184 }
185
186 pub fn with_numlines(self, numlines: bool) -> Self {
190 CodeEditor { numlines, ..self }
191 }
192
193 pub fn with_numlines_shift(self, numlines_shift: isize) -> Self {
197 CodeEditor {
198 numlines_shift,
199 ..self
200 }
201 }
202
203 pub fn with_numlines_only_natural(self, numlines_only_natural: bool) -> Self {
207 CodeEditor {
208 numlines_only_natural,
209 ..self
210 }
211 }
212
213 pub fn with_syntax(self, syntax: Syntax) -> Self {
217 CodeEditor { syntax, ..self }
218 }
219
220 pub fn vscroll(self, vscroll: bool) -> Self {
224 CodeEditor { vscroll, ..self }
225 }
226 pub fn auto_shrink(self, shrink: bool) -> Self {
230 CodeEditor {
231 desired_width: if shrink { 0.0 } else { self.desired_width },
232 ..self
233 }
234 }
235
236 pub fn desired_width(self, width: f32) -> Self {
240 CodeEditor {
241 desired_width: width,
242 ..self
243 }
244 }
245
246 pub fn stick_to_bottom(self, stick_to_bottom: bool) -> Self {
256 CodeEditor {
257 stick_to_bottom,
258 ..self
259 }
260 }
261
262 #[cfg(feature = "egui")]
263 pub fn format_token(&self, ty: TokenType) -> egui::text::TextFormat {
264 format_token(&self.theme, self.fontsize, ty)
265 }
266
267 #[cfg(feature = "egui")]
268 fn numlines_show(&self, ui: &mut egui::Ui, text: &str) {
269 use egui::TextBuffer;
270
271 let total = if text.ends_with('\n') || text.is_empty() {
272 text.lines().count() + 1
273 } else {
274 text.lines().count()
275 }
276 .max(self.rows) as isize;
277 let max_indent = total
278 .to_string()
279 .len()
280 .max(!self.numlines_only_natural as usize * self.numlines_shift.to_string().len());
281 let mut counter = (1..=total)
282 .map(|i| {
283 let num = i + self.numlines_shift;
284 if num <= 0 && self.numlines_only_natural {
285 String::new()
286 } else {
287 let label = num.to_string();
288 format!(
289 "{}{label}",
290 " ".repeat(max_indent.saturating_sub(label.len()))
291 )
292 }
293 })
294 .collect::<Vec<String>>()
295 .join("\n");
296
297 #[allow(clippy::cast_precision_loss)]
298 let width = max_indent as f32
299 * self.fontsize
300 * 0.5
301 * !(total + self.numlines_shift <= 0 && self.numlines_only_natural) as u8 as f32;
302
303 let mut layouter = |ui: &egui::Ui, text_buffer: &dyn TextBuffer, _wrap_width: f32| {
304 let layout_job = egui::text::LayoutJob::single_section(
305 text_buffer.as_str().to_string(),
306 egui::TextFormat::simple(
307 egui::FontId::monospace(self.fontsize),
308 self.theme.type_color(TokenType::Comment(true)),
309 ),
310 );
311 ui.fonts_mut(|f| f.layout_job(layout_job))
312 };
313
314 ui.add(
315 egui::TextEdit::multiline(&mut counter)
316 .id_source(format!("{}_numlines", self.id))
317 .font(egui::TextStyle::Monospace)
318 .interactive(false)
319 .frame(false)
320 .desired_rows(self.rows)
321 .desired_width(width)
322 .layouter(&mut layouter),
323 );
324 }
325
326 #[cfg(feature = "egui")]
327 pub fn show_with_completer(
329 &mut self,
330 ui: &mut egui::Ui,
331 text: &mut dyn egui::TextBuffer,
332 completer: &mut Completer,
333 ) -> TextEditOutput {
334 completer.handle_input(ui.ctx());
335 let mut editor_output = self.show(ui, text);
336 completer.show(&self.syntax, &self.theme, self.fontsize, &mut editor_output);
337 editor_output
338 }
339
340 #[cfg(feature = "egui")]
341 pub fn show(&mut self, ui: &mut egui::Ui, text: &mut dyn egui::TextBuffer) -> TextEditOutput {
343 use egui::TextBuffer;
344
345 let mut text_edit_output: Option<TextEditOutput> = None;
346 let mut code_editor = |ui: &mut egui::Ui| {
347 ui.horizontal_top(|h| {
348 self.theme.modify_style(h, self.fontsize);
349 if self.numlines {
350 self.numlines_show(h, text.as_str());
351 }
352 egui::ScrollArea::horizontal()
353 .id_salt(format!("{}_inner_scroll", self.id))
354 .show(h, |ui| {
355 let mut layouter =
356 |ui: &egui::Ui, text_buffer: &dyn TextBuffer, _wrap_width: f32| {
357 let layout_job = highlight(ui.ctx(), self, text_buffer.as_str());
358 ui.fonts_mut(|f| f.layout_job(layout_job))
359 };
360 let output = egui::TextEdit::multiline(text)
361 .id_source(&self.id)
362 .lock_focus(true)
363 .desired_rows(self.rows)
364 .frame(true)
365 .desired_width(self.desired_width)
366 .layouter(&mut layouter)
367 .show(ui);
368 text_edit_output = Some(output);
369 });
370 });
371 };
372 if self.vscroll {
373 egui::ScrollArea::vertical()
374 .id_salt(format!("{}_outer_scroll", self.id))
375 .stick_to_bottom(self.stick_to_bottom)
376 .show(ui, code_editor);
377 } else {
378 code_editor(ui);
379 }
380
381 text_edit_output.expect("TextEditOutput should exist at this point")
382 }
383}
384
385#[cfg(feature = "editor")]
386#[cfg(feature = "egui")]
387impl Editor for CodeEditor {
388 fn append(&self, job: &mut LayoutJob, token: &Token) {
389 if !token.buffer().is_empty() {
390 job.append(token.buffer(), 0.0, self.format_token(token.ty()));
391 }
392 }
393
394 fn syntax(&self) -> &Syntax {
395 &self.syntax
396 }
397}
398
399#[cfg(feature = "egui")]
400pub fn format_token(theme: &ColorTheme, fontsize: f32, ty: TokenType) -> egui::text::TextFormat {
401 let font_id = egui::FontId::monospace(fontsize);
402 let color = theme.type_color(ty);
403 egui::text::TextFormat::simple(font_id, color)
404}